Piper makes integrating external tools into Burp easier.
2 |
In true Unix
3 | fashion, small things that do one thing and do it well can be connected
4 | together like building blocks to aid quick experimentation. By lowering
5 | the level of coupling, any programming language can be used as long as
6 | it can accept inputs on the standard input or as command line
7 | parameters.
8 |
Extending message editors for new formats or integrating a
9 | better comparator/diff tool is as simple as filling a short form with
10 | the command line and some details with sensible defaults.
11 |
Useful configs
12 | can be shared using the the textual YAML format, the defaults already
13 | contain some simple examples.
14 |
--------------------------------------------------------------------------------
/src/main/java/com/redpois0n/terminal/TerminalCaret.java:
--------------------------------------------------------------------------------
1 | package com.redpois0n.terminal;
2 |
3 | import java.awt.Color;
4 | import java.awt.Graphics;
5 | import java.awt.Rectangle;
6 |
7 | import javax.swing.text.DefaultCaret;
8 | import javax.swing.text.JTextComponent;
9 |
10 | @SuppressWarnings("serial")
11 | public class TerminalCaret extends DefaultCaret {
12 |
13 | protected synchronized void damage(Rectangle r) {
14 | if (r == null)
15 | return;
16 |
17 | x = r.x;
18 | y = r.y;
19 | height = r.height;
20 |
21 | if (width <= 0) {
22 | width = getComponent().getWidth();
23 | }
24 |
25 | repaint();
26 | }
27 |
28 | public void paint(Graphics g) {
29 | JTextComponent comp = getComponent();
30 |
31 | if (comp == null) {
32 | return;
33 | }
34 |
35 | int dot = getDot();
36 | Rectangle r = null;
37 | char dotChar;
38 |
39 | try {
40 | r = comp.modelToView(dot);
41 | if (r == null) {
42 | return;
43 | }
44 | dotChar = comp.getText(dot, 1).charAt(0);
45 | } catch (Exception e) {
46 | e.printStackTrace();
47 | return;
48 | }
49 |
50 | if ((x != r.x) || (y != r.y)) {
51 | repaint();
52 | x = r.x;
53 | y = r.y;
54 | height = r.height;
55 | }
56 |
57 | g.setColor(Color.white);
58 | g.setXORMode(comp.getBackground());
59 |
60 | width = g.getFontMetrics().charWidth(dotChar);
61 |
62 | if (isVisible()) {
63 | g.fillRect(r.x, r.y, width, r.height);
64 | }
65 | }
66 | }
--------------------------------------------------------------------------------
/gradlew.bat:
--------------------------------------------------------------------------------
1 | @if "%DEBUG%" == "" @echo off
2 | @rem ##########################################################################
3 | @rem
4 | @rem Gradle startup script for Windows
5 | @rem
6 | @rem ##########################################################################
7 |
8 | @rem Set local scope for the variables with windows NT shell
9 | if "%OS%"=="Windows_NT" setlocal
10 |
11 | set DIRNAME=%~dp0
12 | if "%DIRNAME%" == "" set DIRNAME=.
13 | set APP_BASE_NAME=%~n0
14 | set APP_HOME=%DIRNAME%
15 |
16 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
17 | set DEFAULT_JVM_OPTS=
18 |
19 | @rem Find java.exe
20 | if defined JAVA_HOME goto findJavaFromJavaHome
21 |
22 | set JAVA_EXE=java.exe
23 | %JAVA_EXE% -version >NUL 2>&1
24 | if "%ERRORLEVEL%" == "0" goto init
25 |
26 | echo.
27 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
28 | echo.
29 | echo Please set the JAVA_HOME variable in your environment to match the
30 | echo location of your Java installation.
31 |
32 | goto fail
33 |
34 | :findJavaFromJavaHome
35 | set JAVA_HOME=%JAVA_HOME:"=%
36 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe
37 |
38 | if exist "%JAVA_EXE%" goto init
39 |
40 | echo.
41 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
42 | echo.
43 | echo Please set the JAVA_HOME variable in your environment to match the
44 | echo location of your Java installation.
45 |
46 | goto fail
47 |
48 | :init
49 | @rem Get command-line arguments, handling Windows variants
50 |
51 | if not "%OS%" == "Windows_NT" goto win9xME_args
52 |
53 | :win9xME_args
54 | @rem Slurp the command line arguments.
55 | set CMD_LINE_ARGS=
56 | set _SKIP=2
57 |
58 | :win9xME_args_slurp
59 | if "x%~1" == "x" goto execute
60 |
61 | set CMD_LINE_ARGS=%*
62 |
63 | :execute
64 | @rem Setup the command line
65 |
66 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
67 |
68 | @rem Execute Gradle
69 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS%
70 |
71 | :end
72 | @rem End local scope for the variables with windows NT shell
73 | if "%ERRORLEVEL%"=="0" goto mainEnd
74 |
75 | :fail
76 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
77 | rem the _cmd.exe /c_ return code!
78 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
79 | exit /b 1
80 |
81 | :mainEnd
82 | if "%OS%"=="Windows_NT" endlocal
83 |
84 | :omega
85 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | Piper for Burp Suite
2 | ====================
3 |
4 | Piper integrates external tools and their pipelines to Burp Suite. The
5 | extension can pass HTTP requests and responses from Burp to external programs,
6 | then feed the execution result back to Burp. With Piper you can create:
7 |
8 | * **Commentators**: Display the output of an external program in Proxy History
9 | as comments. For example, you can display the cryptographic hash of every
10 | request by piping their content to `sha256sum`.
11 | * **Highlighters**: Highlight items in the proxy history based on their
12 | contents. For example, you can highlight items where HTTP response includes
13 | elements of a wordlist.
14 | * **Message Viewers**: Display the contents of HTTP messages with custom
15 | formatting. For example, you can display Protobuf structures by piping message
16 | contents to `protoc`.
17 | * **Context Menu Items**: Invoke external tools from context menu. For example,
18 | you can use an external diff GUI to compare HTTP messages.
19 | * **Intruder Payload Generators**: Generate payloads for Intruder with external
20 | tools. For example, you can make Intruder use password candidates generated by
21 | John the Ripper.
22 | * **Intruder Payload Processors**: Transform Intruder payloads. For example, you
23 | can apply base64 encoding with a custom alphabet using an external script.
24 | * **Macros**: You can use external tools as part of Macros. For example, you
25 | can automatically generate predictable CSRF tokens for every outgoing request.
26 | * **HTTP Listeners**: Transform outgoing and incoming HTTP messages. For
27 | example, you can use an external Python script to handle custom encryption.
28 |
29 | Detailed usage information is provided in the original [GWAPT Gold
30 | Paper](https://www.sans.org/white-papers/39440/), and in [this demonstration
31 | video](https://vimeo.com/401007109).
32 |
33 | Building
34 | --------
35 |
36 | Execute `./gradlew build` and you'll have the plugin ready in
37 | `build/libs/burp-piper.jar`
38 |
39 | Known issues
40 | ------------
41 |
42 | - The terminal emulator ignores background color when _Look and feel_ is set
43 | to _Nimbus_, see https://bugs.openjdk.java.net/browse/JDK-8058704
44 |
45 | Security
46 | --------
47 |
48 | Piper configurations can be exported and imported. As configurations define
49 | commands to be executed on the user's machine, importing malicious
50 | configurations is a security risk.
51 |
52 | Piper disables configurations loaded via the GUI to prevent exploitation, and
53 | unexpected behavior (e.g.: modification of HTTP messages). To support
54 | automation, Piper enables configurations loaded via the `PIPER_CONFIG`
55 | environment variable, so extra care must be taken in this use case.
56 |
57 | Users should always review configurations before importing or enabling them.
58 |
59 | License
60 | -------
61 |
62 | The whole project is available under the GNU General Public License v3.0,
63 | see `LICENSE.md`. The [swing-terminal component][1] was developed by
64 | @redpois0n, released under this same license.
65 |
66 | [1]: https://github.com/redpois0n/swing-terminal
67 |
--------------------------------------------------------------------------------
/src/main/yaml/defaults.yaml:
--------------------------------------------------------------------------------
1 | messageViewers:
2 | - prefix: [openssl, asn1parse, -inform, DER, -i]
3 | inputMethod: stdin
4 | name: OpenSSL ASN.1 decoder
5 | filter:
6 | orElse:
7 | - prefix: !!binary |-
8 | MII=
9 | - prefix: !!binary |-
10 | MIA=
11 | - prefix: [dumpasn1]
12 | inputMethod: filename
13 | name: DumpASN1
14 | filter:
15 | orElse:
16 | - prefix: !!binary |-
17 | MII=
18 | - prefix: !!binary |-
19 | MIA=
20 | - prefix: [python, -m, json.tool]
21 | inputMethod: stdin
22 | name: Python JSON formatter
23 | filter:
24 | orElse:
25 | - {prefix: '{', postfix: '}'}
26 | - {prefix: '[', postfix: ']'}
27 | - prefix: [hd]
28 | inputMethod: stdin
29 | name: hd
30 | - prefix: [protoc, --decode_raw]
31 | inputMethod: stdin
32 | exitCode: [0]
33 | name: ProtoBuf
34 | - prefix: [gron]
35 | inputMethod: stdin
36 | name: gron
37 | filter: {prefix: '{'}
38 | menuItems:
39 | - prefix: [okular]
40 | inputMethod: filename
41 | name: Okular
42 | filter:
43 | regex:
44 | pattern: ^%PDF-1\.[0-9]
45 | flags: [case insensitive, dotall]
46 | hasGUI: true
47 | - prefix: [feh, -FZ]
48 | inputMethod: filename
49 | name: feh
50 | filter:
51 | cmd:
52 | prefix: [file, -i, '-']
53 | inputMethod: stdin
54 | stdout:
55 | regex: {pattern: image/}
56 | hasGUI: true
57 | maxInputs: 1
58 | - prefix: [urxvt, -e, hexcurse]
59 | inputMethod: filename
60 | name: hexcurse
61 | hasGUI: true
62 | maxInputs: 1
63 | - prefix: [urxvt, -e, vbindiff]
64 | inputMethod: filename
65 | name: vbindiff without headers
66 | hasGUI: true
67 | maxInputs: 2
68 | - prefix: [urxvt, -e, vbindiff]
69 | inputMethod: filename
70 | passHeaders: true
71 | name: vbindiff with headers
72 | hasGUI: true
73 | maxInputs: 2
74 | - prefix: [git, diff, --color=always]
75 | inputMethod: filename
76 | name: git diff
77 | minInputs: 2
78 | maxInputs: 2
79 | - prefix: [git, diff, --color=always, -w]
80 | inputMethod: filename
81 | name: git diff (ignore whitespace)
82 | minInputs: 2
83 | maxInputs: 2
84 | - prefix: [radiff2, -x]
85 | inputMethod: filename
86 | name: radiff2 (two column hexdump diffing)
87 | minInputs: 2
88 | maxInputs: 2
89 | - prefix: [sh, -c, dos2unix | xclip -selection clipboard]
90 | inputMethod: stdin
91 | passHeaders: true
92 | requiredInPath: [dos2unix, xclip]
93 | name: Copy to clipboard with headers without \r
94 | hasGUI: true
95 | - prefix: [meld]
96 | inputMethod: filename
97 | passHeaders: true
98 | name: Meld
99 | hasGUI: true
100 | avoidPipe: true
101 | minInputs: 2
102 | maxInputs: 3
103 | - prefix: [sh, -c, fromdos | xsel -i -b]
104 | inputMethod: stdin
105 | passHeaders: true
106 | requiredInPath: [fromdos, xsel]
107 | name: Copy to clipboard with headers without \r (alt)
108 | hasGUI: true
109 | commentators:
110 | - prefix: [sha256sum]
111 | inputMethod: stdin
112 | name: SHA-256
113 | intruderPayloadProcessors:
114 | - prefix: [sh, -c, dd if=/dev/urandom of=/dev/stdout bs=1 count=`shuf -i1-100 -n1` status=none]
115 | inputMethod: stdin
116 | requiredInPath: [dd, shuf]
117 | name: replace with /dev/urandom
118 | intruderPayloadGenerators:
119 | - prefix: [seq, '5']
120 | inputMethod: stdin
121 | name: seq
122 | - prefix: [python3, /path/to/exrex.py, 'foobar[a-c]{1,2}[0-9]']
123 | inputMethod: stdin
124 | name: exrex
125 |
--------------------------------------------------------------------------------
/src/main/java/org/zeromq/codec/Z85.java:
--------------------------------------------------------------------------------
1 | package org.zeromq.codec;
2 |
3 | public final class Z85
4 | {
5 | private Z85()
6 | {
7 | }
8 |
9 | public static byte[] ENCODER = { '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v',
10 | 'w', 'x', 'y', 'z', 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z', '.', '-',
11 | ':', '+', '=', '^', '!', '/', '*', '?', '&', '<', '>', '(', ')', '[', ']', '{', '}', '@', '%', '$', '#', '0' };
12 |
13 | public static byte[] DECODER = { 0x00, 0x44, 0x00, 0x54, 0x53, 0x52, 0x48, 0x00, 0x4B, 0x4C, 0x46, 0x41, 0x00, 0x3F, 0x3E, 0x45, 0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x40,
14 | 0x00, 0x49, 0x42, 0x4A, 0x47, 0x51, 0x24, 0x25, 0x26, 0x27, 0x28, 0x29, 0x2A, 0x2B, 0x2C, 0x2D, 0x2E, 0x2F, 0x30, 0x31, 0x32, 0x33, 0x34, 0x35, 0x36, 0x37, 0x38,
15 | 0x39, 0x3A, 0x3B, 0x3C, 0x3D, 0x4D, 0x00, 0x4E, 0x43, 0x00, 0x00, 0x0A, 0x0B, 0x0C, 0x0D, 0x0E, 0x0F, 0x10, 0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0x17, 0x18, 0x19,
16 | 0x1A, 0x1B, 0x1C, 0x1D, 0x1E, 0x1F, 0x20, 0x21, 0x22, 0x23, 0x4F, 0x00, 0x50, 0x00, 0x00 };
17 |
18 | /**
19 | * Encode a binary frame as a string using Z85 encoding;
20 | * @param data
21 | * @return
22 | */
23 | public static String Z85Encoder(byte[] data)
24 | {
25 | if (data == null || data.length % 4 != 0) {
26 | return null;
27 | }
28 | int size = data.length;
29 | int char_nbr = 0;
30 | int byte_nbr = 0;
31 | long value = 0;
32 | byte[] dest = new byte[size * 5 / 4];
33 | while (byte_nbr < size) {
34 | // Accumulate value in base 256 (binary)
35 | value = value * 256 + (data[byte_nbr++] & 0xFF); // Convert signed
36 | // byte to int
37 | if (byte_nbr % 4 == 0) {
38 | // Output value in base 85
39 | long divisor = 85 * 85 * 85 * 85;
40 | while (divisor > 0) {
41 | int index = (int) (value / divisor % 85);
42 | dest[char_nbr++] = ENCODER[index];
43 | divisor /= 85;
44 | }
45 | value = 0;
46 | }
47 | }
48 | return new String(dest);
49 | }
50 |
51 | public static byte[] Z85Decoder(String string)
52 | {
53 | // Accepts only strings bounded to 5 bytes
54 | if (string == null || string.length() % 5 != 0)
55 | return null;
56 |
57 | int decoded_size = string.length() * 4 / 5;
58 | byte[] decoded = new byte[decoded_size];
59 |
60 | int byte_nbr = 0;
61 | int char_nbr = 0;
62 | long value = 0;
63 | while (char_nbr < string.length()) {
64 | // Accumulate value in base 85
65 | value = value * 85 + DECODER[(byte) string.charAt(char_nbr++) - 32];
66 | if (char_nbr % 5 == 0) {
67 | // Output value in base 256
68 | long divisor = 256 * 256 * 256;
69 | while (divisor > 0) {
70 | decoded[byte_nbr++] = (byte) (value / divisor % 256);
71 | divisor /= 256;
72 | }
73 | value = 0;
74 | }
75 | }
76 | return decoded;
77 | }
78 | }
79 |
--------------------------------------------------------------------------------
/src/main/kotlin/burp/Editors.kt:
--------------------------------------------------------------------------------
1 | package burp
2 |
3 | import com.redpois0n.terminal.JTerminal
4 | import java.awt.Component
5 | import javax.swing.JScrollPane
6 | import javax.swing.SwingUtilities
7 | import kotlin.concurrent.thread
8 |
9 | abstract class Editor(private val tool: Piper.MessageViewer,
10 | protected val helpers: IExtensionHelpers,
11 | private val callbacks: IBurpExtenderCallbacks) : IMessageEditorTab {
12 | private var msg: ByteArray? = null
13 |
14 | override fun getMessage(): ByteArray? = msg
15 | override fun isModified(): Boolean = false
16 | override fun getTabCaption(): String = tool.common.name
17 |
18 | override fun isEnabled(content: ByteArray?, isRequest: Boolean): Boolean {
19 | if (content == null || !tool.common.isInToolScope(isRequest)) return false
20 |
21 | val rr = RequestResponse.fromBoolean(isRequest)
22 | val payload = getPayload(content, rr)
23 |
24 | if (payload.isEmpty()) return false
25 |
26 | if (!tool.common.hasFilter()) {
27 | val cmd = tool.common.cmd
28 | return !cmd.hasFilter || cmd.matches(payload, helpers, callbacks) // TODO cache output
29 | }
30 |
31 | val mi = MessageInfo(payload, helpers.bytesToString(payload), rr.getHeaders(content, helpers), url = null)
32 | return tool.common.filter.matches(mi, helpers, callbacks)
33 | }
34 |
35 | override fun setMessage(content: ByteArray?, isRequest: Boolean) {
36 | msg = content
37 | if (content == null) return
38 | thread(start = true) {
39 | val input = getPayload(content, RequestResponse.fromBoolean(isRequest))
40 | tool.common.cmd.execute(input).processOutput(this::outputProcessor)
41 | }
42 | }
43 |
44 | private fun getPayload(content: ByteArray, rr: RequestResponse) =
45 | if (tool.common.cmd.passHeaders) content
46 | else content.copyOfRange(rr.getBodyOffset(content, helpers), content.size)
47 |
48 | abstract fun outputProcessor(process: Process)
49 |
50 | abstract override fun getSelectedData(): ByteArray
51 | abstract override fun getUiComponent(): Component
52 | }
53 |
54 | class TerminalEditor(tool: Piper.MessageViewer, helpers: IExtensionHelpers, callbacks: IBurpExtenderCallbacks) : Editor(tool, helpers, callbacks) {
55 | private val terminal = JTerminal()
56 | private val scrollPane = JScrollPane()
57 |
58 | init {
59 | scrollPane.setViewportView(terminal)
60 | }
61 |
62 | override fun getSelectedData(): ByteArray = helpers.stringToBytes(terminal.selectedText)
63 | override fun getUiComponent(): Component = terminal
64 |
65 | override fun outputProcessor(process: Process) {
66 | terminal.text = ""
67 | for (stream in arrayOf(process.inputStream, process.errorStream)) {
68 | thread {
69 | val reader = stream.bufferedReader()
70 | while (true) {
71 | val line = reader.readLine() ?: break
72 | terminal.append("$line\n")
73 | }
74 | }.start()
75 | }
76 | }
77 | }
78 |
79 | class TextEditor(tool: Piper.MessageViewer, helpers: IExtensionHelpers,
80 | callbacks: IBurpExtenderCallbacks) : Editor(tool, helpers, callbacks) {
81 | private val editor = callbacks.createTextEditor()
82 |
83 | init {
84 | editor.setEditable(false)
85 | }
86 |
87 | override fun getSelectedData(): ByteArray = editor.selectedText
88 | override fun getUiComponent(): Component = editor.component
89 |
90 | override fun outputProcessor(process: Process) {
91 | process.inputStream.use {
92 | val bytes = it.readBytes()
93 | SwingUtilities.invokeLater { editor.text = bytes }
94 | }
95 | }
96 | }
--------------------------------------------------------------------------------
/src/main/proto/burp/piper.proto:
--------------------------------------------------------------------------------
1 | /*
2 | * This file is part of Piper for Burp Suite (https://github.com/silentsignal/burp-piper)
3 | * Copyright (c) 2018 Andras Veres-Szentkiralyi
4 | *
5 | * This program is free software: you can redistribute it and/or modify
6 | * it under the terms of the GNU General Public License as published by
7 | * the Free Software Foundation, version 3 of the License, or
8 | * (at your option) any later version.
9 | *
10 | * This program is distributed in the hope that it will be useful,
11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of
12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 | * GNU General Public License for more details.
14 | *
15 | * You should have received a copy of the GNU General Public License
16 | * along with this program. If not, see .
17 | */
18 |
19 | syntax = "proto3";
20 |
21 | package burp;
22 |
23 | message RegularExpression {
24 | string pattern = 1;
25 | int32 flags = 2;
26 | }
27 |
28 | message HeaderMatch {
29 | string header = 1;
30 | RegularExpression regex = 2;
31 | }
32 |
33 | message CommandInvocation {
34 | repeated string prefix = 1;
35 | repeated string postfix = 2;
36 | enum InputMethod {
37 | STDIN = 0;
38 | FILENAME = 1;
39 | }
40 | InputMethod inputMethod = 3;
41 | bool passHeaders = 4;
42 | repeated string requiredInPath = 5;
43 | repeated int32 exitCode = 6;
44 | MessageMatch stdout = 7;
45 | MessageMatch stderr = 8;
46 | }
47 |
48 | message MessageMatch {
49 | bytes prefix = 1;
50 | bytes postfix = 2;
51 | RegularExpression regex = 3;
52 | HeaderMatch header = 4;
53 | CommandInvocation cmd = 5;
54 | bool negation = 6;
55 | repeated MessageMatch andAlso = 7;
56 | repeated MessageMatch orElse = 8;
57 | bool inScope = 9;
58 | }
59 |
60 | message MinimalTool {
61 | string name = 1;
62 | CommandInvocation cmd = 2;
63 | MessageMatch filter = 3;
64 | bool enabled = 4;
65 | enum Scope {
66 | REQUEST_RESPONSE = 0;
67 | REQUEST_ONLY = 1;
68 | RESPONSE_ONLY = 2;
69 | }
70 | Scope scope = 5;
71 | }
72 |
73 | message UserActionTool {
74 | MinimalTool common = 1;
75 | bool hasGUI = 2;
76 | int32 maxInputs = 3;
77 | int32 minInputs = 4;
78 | bool avoidPipe = 5;
79 | }
80 |
81 | message MessageViewer {
82 | MinimalTool common = 1;
83 | bool usesColors = 2;
84 | }
85 |
86 | enum HttpListenerScope {
87 | REQUEST = 0;
88 | RESPONSE = 1;
89 | RESPONSE_WITH_REQUEST = 2;
90 | }
91 |
92 | message HttpListener {
93 | MinimalTool common = 1;
94 | int32 tool = 2;
95 | HttpListenerScope scope = 3;
96 | bool ignoreOutput = 4;
97 | }
98 |
99 | message Commentator {
100 | MinimalTool common = 1;
101 | // 2 was RequestResponse
102 | bool overwrite = 3;
103 | bool applyWithListener = 4;
104 | }
105 |
106 | message Highlighter {
107 | MinimalTool common = 1;
108 | string color = 2;
109 | bool overwrite = 3;
110 | bool applyWithListener = 4;
111 | }
112 |
113 | message Config {
114 | repeated MinimalTool macro = 1;
115 | repeated MessageViewer messageViewer = 2;
116 | repeated UserActionTool menuItem = 3;
117 | repeated HttpListener httpListener = 4;
118 | repeated Commentator commentator = 5;
119 | bool developer = 6;
120 | repeated MinimalTool intruderPayloadProcessor = 7;
121 | repeated Highlighter highlighter = 8;
122 | repeated MinimalTool intruderPayloadGenerator = 9;
123 | }
124 |
125 | message MimeTypes {
126 | repeated Type type = 1;
127 |
128 | message Type {
129 | string name = 1;
130 | repeated Subtype subtype = 2;
131 | }
132 |
133 | message Subtype {
134 | string name = 1;
135 | string extension = 2;
136 | }
137 | }
--------------------------------------------------------------------------------
/gradlew:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env sh
2 |
3 | ##############################################################################
4 | ##
5 | ## Gradle start up script for UN*X
6 | ##
7 | ##############################################################################
8 |
9 | # Attempt to set APP_HOME
10 | # Resolve links: $0 may be a link
11 | PRG="$0"
12 | # Need this for relative symlinks.
13 | while [ -h "$PRG" ] ; do
14 | ls=`ls -ld "$PRG"`
15 | link=`expr "$ls" : '.*-> \(.*\)$'`
16 | if expr "$link" : '/.*' > /dev/null; then
17 | PRG="$link"
18 | else
19 | PRG=`dirname "$PRG"`"/$link"
20 | fi
21 | done
22 | SAVED="`pwd`"
23 | cd "`dirname \"$PRG\"`/" >/dev/null
24 | APP_HOME="`pwd -P`"
25 | cd "$SAVED" >/dev/null
26 |
27 | APP_NAME="Gradle"
28 | APP_BASE_NAME=`basename "$0"`
29 |
30 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
31 | DEFAULT_JVM_OPTS=""
32 |
33 | # Use the maximum available, or set MAX_FD != -1 to use that value.
34 | MAX_FD="maximum"
35 |
36 | warn () {
37 | echo "$*"
38 | }
39 |
40 | die () {
41 | echo
42 | echo "$*"
43 | echo
44 | exit 1
45 | }
46 |
47 | # OS specific support (must be 'true' or 'false').
48 | cygwin=false
49 | msys=false
50 | darwin=false
51 | nonstop=false
52 | case "`uname`" in
53 | CYGWIN* )
54 | cygwin=true
55 | ;;
56 | Darwin* )
57 | darwin=true
58 | ;;
59 | MINGW* )
60 | msys=true
61 | ;;
62 | NONSTOP* )
63 | nonstop=true
64 | ;;
65 | esac
66 |
67 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
68 |
69 | # Determine the Java command to use to start the JVM.
70 | if [ -n "$JAVA_HOME" ] ; then
71 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
72 | # IBM's JDK on AIX uses strange locations for the executables
73 | JAVACMD="$JAVA_HOME/jre/sh/java"
74 | else
75 | JAVACMD="$JAVA_HOME/bin/java"
76 | fi
77 | if [ ! -x "$JAVACMD" ] ; then
78 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
79 |
80 | Please set the JAVA_HOME variable in your environment to match the
81 | location of your Java installation."
82 | fi
83 | else
84 | JAVACMD="java"
85 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
86 |
87 | Please set the JAVA_HOME variable in your environment to match the
88 | location of your Java installation."
89 | fi
90 |
91 | # Increase the maximum file descriptors if we can.
92 | if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then
93 | MAX_FD_LIMIT=`ulimit -H -n`
94 | if [ $? -eq 0 ] ; then
95 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
96 | MAX_FD="$MAX_FD_LIMIT"
97 | fi
98 | ulimit -n $MAX_FD
99 | if [ $? -ne 0 ] ; then
100 | warn "Could not set maximum file descriptor limit: $MAX_FD"
101 | fi
102 | else
103 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT"
104 | fi
105 | fi
106 |
107 | # For Darwin, add options to specify how the application appears in the dock
108 | if $darwin; then
109 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
110 | fi
111 |
112 | # For Cygwin, switch paths to Windows format before running java
113 | if $cygwin ; then
114 | APP_HOME=`cygpath --path --mixed "$APP_HOME"`
115 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
116 | JAVACMD=`cygpath --unix "$JAVACMD"`
117 |
118 | # We build the pattern for arguments to be converted via cygpath
119 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`
120 | SEP=""
121 | for dir in $ROOTDIRSRAW ; do
122 | ROOTDIRS="$ROOTDIRS$SEP$dir"
123 | SEP="|"
124 | done
125 | OURCYGPATTERN="(^($ROOTDIRS))"
126 | # Add a user-defined pattern to the cygpath arguments
127 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then
128 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)"
129 | fi
130 | # Now convert the arguments - kludge to limit ourselves to /bin/sh
131 | i=0
132 | for arg in "$@" ; do
133 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -`
134 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option
135 |
136 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition
137 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"`
138 | else
139 | eval `echo args$i`="\"$arg\""
140 | fi
141 | i=$((i+1))
142 | done
143 | case $i in
144 | (0) set -- ;;
145 | (1) set -- "$args0" ;;
146 | (2) set -- "$args0" "$args1" ;;
147 | (3) set -- "$args0" "$args1" "$args2" ;;
148 | (4) set -- "$args0" "$args1" "$args2" "$args3" ;;
149 | (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
150 | (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
151 | (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
152 | (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
153 | (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
154 | esac
155 | fi
156 |
157 | # Escape application args
158 | save () {
159 | for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done
160 | echo " "
161 | }
162 | APP_ARGS=$(save "$@")
163 |
164 | # Collect all arguments for the java command, following the shell quoting and substitution rules
165 | eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS"
166 |
167 | # by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong
168 | if [ "$(uname)" = "Darwin" ] && [ "$HOME" = "$PWD" ]; then
169 | cd "$(dirname "$0")"
170 | fi
171 |
172 | exec "$JAVACMD" "$@"
173 |
--------------------------------------------------------------------------------
/src/main/kotlin/burp/Enums.kt:
--------------------------------------------------------------------------------
1 | package burp
2 |
3 | import org.snakeyaml.engine.v1.api.Dump
4 | import org.snakeyaml.engine.v1.api.DumpSettingsBuilder
5 | import java.awt.Color
6 | import java.util.*
7 | import java.util.regex.Pattern
8 |
9 | @Suppress("UNUSED", "SpellCheckingInspection")
10 | enum class RegExpFlag {
11 | CASE_INSENSITIVE, MULTILINE, DOTALL, UNICODE_CASE, CANON_EQ,
12 | UNIX_LINES, LITERAL, UNICODE_CHARACTER_CLASS, COMMENTS;
13 |
14 | val value = Pattern::class.java.getField(name).getInt(null)
15 |
16 | override fun toString(): String {
17 | return name.toLowerCase().replace('_', ' ')
18 | }
19 | }
20 |
21 | enum class RequestResponse(val isRequest: Boolean, val contexts: Set) {
22 | REQUEST(isRequest = true, contexts = setOf(IContextMenuInvocation.CONTEXT_MESSAGE_EDITOR_REQUEST,
23 | IContextMenuInvocation.CONTEXT_MESSAGE_VIEWER_REQUEST)) {
24 | override fun getMessage(rr: IHttpRequestResponse): ByteArray? = rr.request
25 |
26 | override fun setMessage(rr: IHttpRequestResponse, value: ByteArray) {
27 | rr.request = value
28 | }
29 |
30 | override fun getBodyOffset(data: ByteArray, helpers: IExtensionHelpers): Int =
31 | helpers.analyzeRequest(data).bodyOffset
32 |
33 | override fun getHeaders(data: ByteArray, helpers: IExtensionHelpers): List =
34 | helpers.analyzeRequest(data).headers
35 | },
36 |
37 | RESPONSE(isRequest = false, contexts = setOf(IContextMenuInvocation.CONTEXT_MESSAGE_EDITOR_RESPONSE,
38 | IContextMenuInvocation.CONTEXT_MESSAGE_VIEWER_RESPONSE)) {
39 | override fun getMessage(rr: IHttpRequestResponse): ByteArray? = rr.response
40 |
41 | override fun setMessage(rr: IHttpRequestResponse, value: ByteArray) {
42 | rr.response = value
43 | }
44 |
45 | override fun getBodyOffset(data: ByteArray, helpers: IExtensionHelpers): Int =
46 | helpers.analyzeResponse(data).bodyOffset
47 |
48 | override fun getHeaders(data: ByteArray, helpers: IExtensionHelpers): List =
49 | helpers.analyzeResponse(data).headers
50 | };
51 |
52 | abstract fun getMessage(rr: IHttpRequestResponse): ByteArray?
53 | abstract fun setMessage(rr: IHttpRequestResponse, value: ByteArray)
54 | abstract fun getBodyOffset(data: ByteArray, helpers: IExtensionHelpers): Int
55 | abstract fun getHeaders(data: ByteArray, helpers: IExtensionHelpers): List
56 |
57 | companion object {
58 | fun fromBoolean(isRequest: Boolean) = if (isRequest) REQUEST else RESPONSE
59 | }
60 | }
61 |
62 | @Suppress("SpellCheckingInspection")
63 | enum class BurpTool {
64 | SUITE, TARGET, PROXY, SPIDER, SCANNER, INTRUDER, REPEATER, SEQUENCER, DECODER, COMPARER, EXTENDER;
65 |
66 | val value = IBurpExtenderCallbacks::class.java.getField("TOOL_$name").getInt(null)
67 |
68 | override fun toString(): String {
69 | return name.toLowerCase().capitalize()
70 | }
71 | }
72 |
73 | enum class MatchNegation(val negation: Boolean, private val description: String) {
74 | NORMAL(false, "Match when all the rules below apply"),
75 | NEGATED(true, "Match when none of the rules below apply");
76 |
77 | override fun toString(): String = description
78 | }
79 |
80 | enum class Highlight(val color: Color?, val textColor: Color = Color.BLACK) {
81 | CLEAR(null),
82 | RED( Color(0xFF, 0x64, 0x64), Color.WHITE),
83 | ORANGE( Color(0xFF, 0xC8, 0x64) ),
84 | YELLOW( Color(0xFF, 0xFF, 0x64) ),
85 | GREEN( Color(0x64, 0xFF, 0x64) ),
86 | CYAN( Color(0x64, 0xFF, 0xFF) ),
87 | BLUE( Color(0x64, 0x64, 0xFF), Color.WHITE),
88 | PINK( Color(0xFF, 0xC8, 0xC8) ),
89 | MAGENTA(Color(0xFF, 0x64, 0xFF) ),
90 | GRAY( Color(0xB4, 0xB4, 0xB4) );
91 |
92 | override fun toString(): String = super.toString().toLowerCase()
93 |
94 | val burpValue: String? get() = if (color == null) null else toString()
95 |
96 | companion object {
97 | private val lookupTable = values().associateBy(Highlight::toString)
98 |
99 | fun fromString(value: String): Highlight? = lookupTable[value]
100 | }
101 | }
102 |
103 | enum class ConfigHttpListenerScope(val hls: Piper.HttpListenerScope, val inputList: List) {
104 | REQUEST (Piper.HttpListenerScope.REQUEST, Collections.singletonList(RequestResponse.REQUEST)),
105 | RESPONSE(Piper.HttpListenerScope.RESPONSE, Collections.singletonList(RequestResponse.RESPONSE)),
106 | RESPONSE_WITH_REQUEST(Piper.HttpListenerScope.RESPONSE_WITH_REQUEST,
107 | listOf(RequestResponse.REQUEST, RequestResponse.RESPONSE)) {
108 | override fun toString(): String = "HTTP responses with request prepended"
109 | };
110 |
111 | override fun toString(): String = "HTTP ${hls.toString().toLowerCase()}s"
112 |
113 | companion object {
114 | fun fromHttpListenerScope(hls: Piper.HttpListenerScope): ConfigHttpListenerScope = values().first { it.hls == hls }
115 | }
116 | }
117 |
118 | enum class ConfigMinimalToolScope(val scope: Piper.MinimalTool.Scope) {
119 | REQUEST_RESPONSE(Piper.MinimalTool.Scope.REQUEST_RESPONSE) {
120 | override fun toString(): String = "HTTP requests and responses"
121 | },
122 | REQUEST_ONLY(Piper.MinimalTool.Scope.REQUEST_ONLY) {
123 | override fun toString(): String = "HTTP requests only"
124 | },
125 | RESPONSE_ONLY(Piper.MinimalTool.Scope.RESPONSE_ONLY) {
126 | override fun toString(): String = "HTTP responses only"
127 | };
128 |
129 | companion object {
130 | fun fromScope(scope: Piper.MinimalTool.Scope): ConfigMinimalToolScope = values().first { it.scope == scope }
131 | }
132 | }
133 |
134 | enum class ConfigFormat {
135 | YAML {
136 | override fun parse(blob: ByteArray): Piper.Config = configFromYaml(String(blob, Charsets.UTF_8))
137 | override fun serialize(config: Piper.Config): ByteArray =
138 | Dump(DumpSettingsBuilder().build()).dumpToString(config.toSettings()).toByteArray(/* default is UTF-8 */)
139 |
140 | override val fileExtension: String
141 | get() = "yaml"
142 | },
143 |
144 | PROTOBUF {
145 | override fun parse(blob: ByteArray): Piper.Config = Piper.Config.parseFrom(blob).updateEnabled(false)
146 | override fun serialize(config: Piper.Config): ByteArray = config.updateEnabled(false).toByteArray()
147 | override val fileExtension: String
148 | get() = "pb"
149 | };
150 |
151 | abstract fun serialize(config: Piper.Config): ByteArray
152 | abstract fun parse(blob: ByteArray): Piper.Config
153 | abstract val fileExtension: String
154 | }
155 |
156 | enum class MessageInfoMatchStrategy {
157 | ANY { override fun predicate(objects: List, check: (MessageInfo) -> Boolean): Boolean = objects.any(check) },
158 | ALL { override fun predicate(objects: List, check: (MessageInfo) -> Boolean): Boolean = objects.all(check) };
159 |
160 | abstract fun predicate(objects: List, check: (MessageInfo) -> Boolean): Boolean
161 | }
162 |
163 | enum class CommandInvocationPurpose {
164 | EXECUTE_ONLY,
165 | SELF_FILTER,
166 | MATCH_FILTER;
167 | }
--------------------------------------------------------------------------------
/src/main/java/com/redpois0n/terminal/JTerminal.java:
--------------------------------------------------------------------------------
1 | package com.redpois0n.terminal;
2 |
3 | import java.awt.Color;
4 | import java.awt.Font;
5 | import java.awt.event.KeyEvent;
6 | import java.awt.event.KeyListener;
7 | import java.util.ArrayList;
8 | import java.util.HashMap;
9 | import java.util.List;
10 | import java.util.Map;
11 |
12 | import javax.swing.JTextPane;
13 | import javax.swing.text.SimpleAttributeSet;
14 | import javax.swing.text.StyleConstants;
15 | import javax.swing.text.StyleContext;
16 | import javax.swing.text.StyledDocument;
17 |
18 | @SuppressWarnings("serial")
19 | public class JTerminal extends JTextPane {
20 |
21 | public static final String RESET = "0";
22 | public static final String BOLD = "1";
23 | public static final String DIM = "2";
24 | public static final String UNDERLINED = "4";
25 | public static final String INVERTED = "7";
26 | public static final String HIDDEN = "8";
27 |
28 | public static final Font DEFAULT_FONT;
29 | public static final Color DEFAULT_FOREGROUND = Color.white;
30 | public static final Color DEFAULT_BACKGROUND = Color.black;
31 | public static final char NULL_CHAR = '\u0000';
32 |
33 | public static final char ESCAPE = 27;
34 | public static final String UNIX_CLEAR = ESCAPE + "[H" + ESCAPE + "[J";
35 |
36 | public static final Map COLORS = new HashMap();
37 |
38 | static {
39 | DEFAULT_FONT = new Font("monospaced", Font.PLAIN, 14);
40 |
41 | // Default colors
42 | COLORS.put("30", Color.black);
43 | COLORS.put("31", Color.red.darker());
44 | COLORS.put("32", Color.green.darker());
45 | COLORS.put("33", Color.yellow.darker());
46 | COLORS.put("34", Color.blue);
47 | COLORS.put("35", Color.magenta.darker());
48 | COLORS.put("36", Color.cyan.darker());
49 | COLORS.put("37", Color.lightGray);
50 | COLORS.put("39", DEFAULT_FOREGROUND);
51 |
52 | // Bright colors
53 | COLORS.put("90", Color.gray);
54 | COLORS.put("91", Color.red);
55 | COLORS.put("92", Color.green);
56 | COLORS.put("93", Color.yellow);
57 | COLORS.put("94", Color.blue.brighter());
58 | COLORS.put("95", Color.magenta);
59 | COLORS.put("96", Color.cyan);
60 | COLORS.put("97", Color.white);
61 |
62 | // Background
63 |
64 | // Default colors
65 | COLORS.put("40", Color.black);
66 | COLORS.put("41", Color.red.darker());
67 | COLORS.put("42", Color.green.darker());
68 | COLORS.put("43", Color.yellow.darker());
69 | COLORS.put("44", Color.blue);
70 | COLORS.put("45", Color.magenta.darker());
71 | COLORS.put("46", Color.cyan.darker());
72 | COLORS.put("47", Color.lightGray);
73 | COLORS.put("49", DEFAULT_FOREGROUND);
74 |
75 | // Bright colors
76 | COLORS.put("100", Color.gray);
77 | COLORS.put("101", Color.red);
78 | COLORS.put("102", Color.green);
79 | COLORS.put("103", Color.yellow);
80 | COLORS.put("104", Color.blue.brighter());
81 | COLORS.put("105", Color.magenta);
82 | COLORS.put("106", Color.cyan);
83 | COLORS.put("107", Color.white);
84 | }
85 |
86 | public static boolean isBackground(String s) {
87 | return s.startsWith("4") || s.startsWith("10");
88 | }
89 |
90 | public static Color getColor(String s) {
91 | Color color = DEFAULT_FOREGROUND;
92 |
93 | boolean bright = s.contains("1;");
94 | s = s.replace("1;", "");
95 |
96 | if (s.endsWith("m")) {
97 | s = s.substring(0, s.length() - 1);
98 | }
99 |
100 | if (COLORS.containsKey(s)) {
101 | color = COLORS.get(s);
102 | }
103 |
104 | if (bright) {
105 | color = color.brighter();
106 | }
107 |
108 | return color;
109 | }
110 |
111 | private List inputListeners = new ArrayList();
112 |
113 | private StyledDocument doc;
114 |
115 | public JTerminal() {
116 | this.doc = getStyledDocument();
117 | setFont(DEFAULT_FONT);
118 | setForeground(DEFAULT_FOREGROUND);
119 | setBackground(DEFAULT_BACKGROUND);
120 | setCaret(new TerminalCaret());
121 |
122 | addKeyListener(new KeyEventListener());
123 | addInputListener(new InputListener() {
124 | @Override
125 | public void processCommand(JTerminal terminal, char c) {
126 |
127 | }
128 | });
129 | }
130 |
131 | /**
132 | * Gets main key listener
133 | * @return
134 | */
135 | public KeyListener getKeyListener() {
136 | return super.getKeyListeners()[0];
137 | }
138 |
139 | public synchronized void append(String s) {
140 | boolean fg = true;
141 | Color foreground = DEFAULT_FOREGROUND;
142 | Color background = DEFAULT_BACKGROUND;
143 | boolean bold = false;
144 | boolean underline = false;
145 | boolean dim = false;
146 |
147 | StringBuilder s1 = new StringBuilder();
148 |
149 | for (int cp = 0; cp < s.toCharArray().length; cp++) {
150 | char c = s.charAt(cp);
151 |
152 | if (c == ESCAPE) {
153 | append(s1.toString(), foreground, background, bold, underline);
154 | char next = s.charAt(cp + 1);
155 |
156 | if (next == '[') {
157 | s1 = new StringBuilder();
158 | cp++;
159 | while ((c = s.charAt(++cp)) != 'm') {
160 | s1.append(c);
161 | }
162 |
163 | String[] attributes = s1.toString().split(";");
164 |
165 | for (String at : attributes) {
166 | if (at.equals(RESET) || s1.length() == 0) {
167 | foreground = DEFAULT_FOREGROUND;
168 | background = DEFAULT_BACKGROUND;
169 | fg = true;
170 | underline = false;
171 | dim = false;
172 | bold = false;
173 | } else if (at.equals(BOLD)) {
174 | bold = !bold;
175 | } else if (at.equals(DIM)) {
176 | dim = !dim;
177 | } else if (at.equals(INVERTED)) {
178 | fg = !fg;
179 | if (fg) {
180 | Color temp = foreground;
181 | foreground = background;
182 | background = temp;
183 | } else {
184 | Color temp = background;
185 | background = foreground;
186 | foreground = temp;
187 | }
188 | } else if (at.equals(UNDERLINED)) {
189 | underline = !underline;
190 | } else if (s1.length() > 0) {
191 | Color color = getColor(at);
192 |
193 | if (isBackground(at)) {
194 | background = color;
195 | } else {
196 | foreground = color;
197 | }
198 |
199 | if (!fg) { // inverted
200 | Color temp = background;
201 | background = foreground;
202 | foreground = temp;
203 | }
204 |
205 | if (dim) {
206 | foreground = foreground.brighter();
207 | }
208 | }
209 | }
210 |
211 | s1 = new StringBuilder();
212 | continue;
213 | }
214 | }
215 |
216 | s1.append(c);
217 | }
218 |
219 | if (s1.length() > 0) {
220 | append(s1.toString(), foreground, background, bold, underline);
221 | }
222 |
223 | setCursorInEnd();
224 |
225 | }
226 |
227 | public void append(String s, Color fg, Color bg, boolean bold, boolean underline) {
228 | StyleContext sc = StyleContext.getDefaultStyleContext();
229 |
230 | setCursorInEnd();
231 |
232 | setCharacterAttributes(sc.addAttribute(SimpleAttributeSet.EMPTY, StyleConstants.Foreground, fg), false);
233 | setCharacterAttributes(sc.addAttribute(SimpleAttributeSet.EMPTY, StyleConstants.Background, bg), false);
234 | setCharacterAttributes(sc.addAttribute(SimpleAttributeSet.EMPTY, StyleConstants.Bold, bold), false);
235 | setCharacterAttributes(sc.addAttribute(SimpleAttributeSet.EMPTY, StyleConstants.Underline, underline), false);
236 |
237 | replaceSelection(s);
238 | }
239 |
240 | public void setCursorInEnd() {
241 | setCaretPosition(doc.getLength());
242 | }
243 |
244 | /**
245 | * Called when key pressed, checks if character is valid and checks for combinations such as Ctrl+C
246 | * @param c
247 | */
248 | public void keyPressed(char c) {
249 | for (InputListener l : inputListeners) {
250 | l.processCommand(this, c);
251 | }
252 | }
253 |
254 | public class KeyEventListener implements KeyListener {
255 |
256 | @Override
257 | public void keyPressed(KeyEvent e) {
258 | if (e.getKeyCode() == KeyEvent.VK_ENTER) {
259 | JTerminal.this.keyPressed('\n');
260 | }
261 | }
262 |
263 | @Override
264 | public void keyReleased(KeyEvent e) {
265 | if (e.getKeyCode() == KeyEvent.VK_CONTROL) {
266 | //ctrl = false;
267 | }
268 | }
269 |
270 | @Override
271 | public void keyTyped(KeyEvent e) {
272 | JTerminal.this.keyPressed(e.getKeyChar());
273 | }
274 | }
275 |
276 | public void addInputListener(InputListener listener) {
277 | inputListeners.add(listener);
278 | }
279 |
280 | public void removeInputListener(InputListener listener) {
281 | inputListeners.remove(listener);
282 | }
283 |
284 | }
285 |
--------------------------------------------------------------------------------
/src/main/kotlin/burp/Extensions.kt:
--------------------------------------------------------------------------------
1 | package burp
2 |
3 | import com.google.protobuf.ByteString
4 | import java.awt.Window
5 | import java.io.File
6 | import java.io.IOException
7 | import java.io.InputStream
8 | import java.lang.RuntimeException
9 | import java.util.*
10 | import java.util.regex.Pattern
11 | import javax.swing.DefaultListModel
12 |
13 | ////////////////////////////////////// GUI //////////////////////////////////////
14 |
15 | fun Piper.MessageMatch.toHumanReadable(negation: Boolean, hideParentheses: Boolean = false): String {
16 | val match = this
17 | val negated = negation xor match.negation
18 | val items = sequence {
19 | if (match.prefix != null && !match.prefix.isEmpty) {
20 | val prefix = if (negated) "doesn't start" else "starts"
21 | yield("$prefix with ${match.prefix.toHumanReadable()}")
22 | }
23 | if (match.postfix != null && !match.postfix.isEmpty) {
24 | val prefix = if (negated) "doesn't end" else "ends"
25 | yield("$prefix with ${match.postfix.toHumanReadable()}")
26 | }
27 | if (match.hasRegex()) yield(match.regex.toHumanReadable(negated))
28 |
29 | if (match.hasHeader()) yield(match.header.toHumanReadable(negated))
30 |
31 | if (match.hasCmd()) yield(match.cmd.toHumanReadable(negated))
32 |
33 | if (match.inScope) yield("request is" + (if (negated) "n't" else "") + " in scope")
34 |
35 | if (match.andAlsoCount > 0) {
36 | yield(match.andAlsoList.joinToString(separator = (if (negated) " or " else " and "),
37 | transform = { it.toHumanReadable(negated) } ))
38 | }
39 |
40 | if (match.orElseCount > 0) {
41 | yield(match.orElseList.joinToString(separator = (if (negated) " and " else " or "),
42 | transform = { it.toHumanReadable(negated) } ))
43 | }
44 | }.toList()
45 | val result = items.joinToString(separator = (if (negated) " or " else " and ")).truncateTo(64)
46 | return if (items.size == 1 || hideParentheses) result else "($result)"
47 | }
48 |
49 | fun Piper.HeaderMatch.toHumanReadable(negation: Boolean): String =
50 | "header \"$header\" " + regex.toHumanReadable(negation)
51 |
52 | fun Piper.CommandInvocation.toHumanReadable(negation: Boolean): String = sequence {
53 | if (this@toHumanReadable.exitCodeCount > 0) {
54 | val nt = if (negation) "n't" else ""
55 | val ecl = this@toHumanReadable.exitCodeList
56 | val values =
57 | if (ecl.size == 1) ecl[0].toString()
58 | else ecl.dropLast(1).joinToString(separator = ", ") + " or ${ecl.last()}"
59 | yield("exit code is$nt $values")
60 | }
61 | if (this@toHumanReadable.hasStdout()) {
62 | yield("stdout " + this@toHumanReadable.stdout.toHumanReadable(negation))
63 | }
64 | if (this@toHumanReadable.hasStderr()) {
65 | yield("stderr " + this@toHumanReadable.stderr.toHumanReadable(negation))
66 | }
67 | }.joinToString(separator = (if (negation) " or " else " and "),
68 | prefix = "when invoking `${this@toHumanReadable.commandLine}`, ")
69 |
70 | val Piper.CommandInvocation.commandLine: String
71 | get() = sequence {
72 | yieldAll(this@commandLine.prefixList.map(::shellQuote))
73 | if (this@commandLine.inputMethod == Piper.CommandInvocation.InputMethod.FILENAME) yield(CMDLINE_INPUT_FILENAME_PLACEHOLDER)
74 | yieldAll(this@commandLine.postfixList.map(::shellQuote))
75 | }.joinToString(separator = " ").truncateTo(64)
76 |
77 | fun shellQuote(s: String): String = if (!s.contains(Regex("[\"\\s\\\\]"))) s
78 | else '"' + s.replace(Regex("[\"\\\\]")) { "\\" + it.groups[0]!!.value } + '"'
79 |
80 | fun String.truncateTo(charLimit: Int): String = if (length < charLimit) this else this.substring(0, charLimit) + "..."
81 |
82 | fun Piper.RegularExpression.toHumanReadable(negation: Boolean): String =
83 | (if (negation) "doesn't match" else "matches") +
84 | " regex \"${this.pattern}\"" +
85 | (if (this.flags == 0) "" else " (${this.flagSet.joinToString(separator = ", ")})")
86 |
87 | fun ByteString.toHumanReadable(): String = if (this.isValidUtf8) '"' + this.toStringUtf8() + '"'
88 | else "bytes " + this.toHexPairs()
89 |
90 | fun ByteString.toHexPairs(): String = this.toByteArray().toHexPairs()
91 |
92 | fun ByteArray.toHexPairs(): String = this.joinToString(separator = ":",
93 | transform = { it.toInt().and(0xFF).toString(16).padStart(2, '0') })
94 |
95 | ////////////////////////////////////// MATCHING //////////////////////////////////////
96 |
97 | fun Piper.MinimalTool.isInToolScope(isRequest: Boolean): Boolean =
98 | when (scope) {
99 | Piper.MinimalTool.Scope.REQUEST_ONLY -> isRequest
100 | Piper.MinimalTool.Scope.RESPONSE_ONLY -> !isRequest
101 | else -> true
102 | }
103 |
104 | fun Piper.MinimalTool.canProcess(md: List, mims: MessageInfoMatchStrategy, helpers: IExtensionHelpers,
105 | callbacks: IBurpExtenderCallbacks): Boolean =
106 | !this.hasFilter() || mims.predicate(md) { this.filter.matches(it, helpers, callbacks) }
107 |
108 | fun Piper.MinimalTool.buildEnabled(value: Boolean? = null): Piper.MinimalTool {
109 | val enabled = value ?: try {
110 | this.cmd.checkDependencies()
111 | true
112 | } catch (_: DependencyException) {
113 | false
114 | }
115 | return toBuilder().setEnabled(enabled).build()
116 | }
117 |
118 | fun Piper.UserActionTool.buildEnabled(value: Boolean? = null): Piper.UserActionTool = toBuilder().setCommon(common.buildEnabled(value)).build()
119 | fun Piper.HttpListener .buildEnabled(value: Boolean? = null): Piper.HttpListener = toBuilder().setCommon(common.buildEnabled(value)).build()
120 | fun Piper.MessageViewer .buildEnabled(value: Boolean? = null): Piper.MessageViewer = toBuilder().setCommon(common.buildEnabled(value)).build()
121 | fun Piper.Commentator .buildEnabled(value: Boolean? = null): Piper.Commentator = toBuilder().setCommon(common.buildEnabled(value)).build()
122 | fun Piper.Highlighter .buildEnabled(value: Boolean? = null): Piper.Highlighter = toBuilder().setCommon(common.buildEnabled(value)).build()
123 |
124 | fun Piper.MessageMatch.matches(message: MessageInfo, helpers: IExtensionHelpers, callbacks: IBurpExtenderCallbacks): Boolean = (
125 | (this.prefix == null || this.prefix.size() == 0 || message.content.startsWith(this.prefix)) &&
126 | (this.postfix == null || this.postfix.size() == 0 || message.content.endsWith(this.postfix)) &&
127 | (!this.hasRegex() || this.regex.matches(message.text)) &&
128 | (!this.hasCmd() || this.cmd.matches(message.content, helpers, callbacks)) &&
129 |
130 | (message.headers == null || !this.hasHeader() || this.header.matches(message.headers)) &&
131 | (message.url == null || !this.inScope || callbacks.isInScope(message.url)) &&
132 |
133 | (this.andAlsoCount == 0 || this.andAlsoList.all { it.matches(message, helpers, callbacks) }) &&
134 | (this.orElseCount == 0 || this.orElseList.any { it.matches(message, helpers, callbacks) })
135 | ) xor this.negation
136 |
137 | fun ByteArray.startsWith(value: ByteString): Boolean {
138 | val mps = value.size()
139 | return this.size >= mps && this.copyOfRange(0, mps) contentEquals value.toByteArray()
140 | }
141 |
142 | fun ByteArray.endsWith(value: ByteString): Boolean {
143 | val mps = value.size()
144 | val mbs = this.size
145 | return mbs >= mps && this.copyOfRange(mbs - mps, mbs) contentEquals value.toByteArray()
146 | }
147 |
148 | private const val DEFAULT_FILE_EXTENSION = ".bin"
149 |
150 | fun Piper.CommandInvocation.execute(vararg inputs: ByteArray): Pair> = execute(*inputs.map { it to null }.toTypedArray())
151 |
152 | fun Piper.CommandInvocation.execute(vararg inputs: Pair): Pair> {
153 | val tempFiles = if (this.inputMethod == Piper.CommandInvocation.InputMethod.FILENAME) {
154 | inputs.map { (contents, extension) ->
155 | File.createTempFile("piper-", extension ?: DEFAULT_FILE_EXTENSION).apply { writeBytes(contents) }
156 | }
157 | } else emptyList()
158 | val args = this.prefixList + tempFiles.map(File::getAbsolutePath) + this.postfixList
159 | val p = Runtime.getRuntime().exec(args.toTypedArray())
160 | if (this.inputMethod == Piper.CommandInvocation.InputMethod.STDIN) {
161 | try {
162 | p.outputStream.use {
163 | inputs.map(Pair::first).forEach(p.outputStream::write)
164 | }
165 | } catch (_: IOException) {
166 | // ignore, see https://github.com/silentsignal/burp-piper/issues/6
167 | }
168 | }
169 | return p to tempFiles
170 | }
171 |
172 | val Piper.CommandInvocationOrBuilder.hasFilter: Boolean
173 | get() = hasStderr() || hasStdout() || exitCodeCount > 0
174 |
175 | fun Piper.CommandInvocation.matches(subject: ByteArray, helpers: IExtensionHelpers, callbacks: IBurpExtenderCallbacks): Boolean {
176 | val (process, tempFiles) = this.execute(subject)
177 | if ((this.hasStderr() && !this.stderr.matches(process.errorStream, helpers, callbacks)) ||
178 | (this.hasStdout() && !this.stdout.matches(process.inputStream, helpers, callbacks))) return false
179 | val exitCode = process.waitFor()
180 | tempFiles.forEach { it.delete() }
181 | return (this.exitCodeCount == 0) || exitCode in this.exitCodeList
182 | }
183 |
184 | fun Piper.MessageMatch.matches(stream: InputStream, helpers: IExtensionHelpers, callbacks: IBurpExtenderCallbacks): Boolean =
185 | this.matches(stream.readBytes(), helpers, callbacks)
186 |
187 | fun Piper.MessageMatch.matches(data: ByteArray, helpers: IExtensionHelpers, callbacks: IBurpExtenderCallbacks): Boolean =
188 | this.matches(MessageInfo(data, helpers.bytesToString(data), headers = null, url = null), helpers, callbacks)
189 |
190 | fun Piper.HeaderMatch.matches(headers: List): Boolean = headers.any {
191 | it.startsWith("${this.header}: ", true) &&
192 | this.regex.matches(it.substring(this.header.length + 2))
193 | }
194 |
195 | fun Piper.RegularExpression.matches(subject: String): Boolean =
196 | this.compile().matcher(subject).find()
197 |
198 | fun Piper.RegularExpression.compile(): Pattern = Pattern.compile(this.pattern, this.flags)
199 |
200 | val Piper.RegularExpression.flagSet: Set
201 | get() = calcEnumSet(RegExpFlag::class.java, RegExpFlag::value, flags, EnumSet.noneOf(RegExpFlag::class.java))
202 |
203 | fun Piper.RegularExpression.Builder.setFlagSet(flags: Set): Piper.RegularExpression.Builder =
204 | this.setFlags(flags.fold(0) { acc: Int, regExpFlag: RegExpFlag -> acc or regExpFlag.value })
205 |
206 | val Piper.HttpListener.toolSet: Set
207 | get() = calcEnumSet(BurpTool::class.java, BurpTool::value, tool, EnumSet.allOf(BurpTool::class.java))
208 |
209 | fun Piper.HttpListener.Builder.setToolSet(tools: Set): Piper.HttpListener.Builder =
210 | this.setTool(tools.fold(0) { acc: Int, tool: BurpTool -> acc or tool.value })
211 |
212 | fun Pair>.processOutput(processor: (Process) -> E): E {
213 | val output = processor(this.first)
214 | this.first.waitFor()
215 | this.second.forEach { it.delete() }
216 | return output
217 | }
218 |
219 | fun > calcEnumSet(enumClass: Class, getter: (E) -> Int, value: Int, zero: Set): Set =
220 | if (value == 0) zero else EnumSet.copyOf(enumClass.enumConstants.filter { (getter(it) and value) != 0 })
221 |
222 | fun DefaultListModel.map(transform: (S) -> T): Iterable = toIterable().map(transform)
223 | fun DefaultListModel.toIterable(): Iterable = (0 until size).map(this::elementAt)
224 |
225 | class DependencyException(dependency: String) : RuntimeException("Dependent executable `$dependency` cannot be found in \$PATH")
226 |
227 | fun Piper.CommandInvocation.checkDependencies() {
228 | val s = sequence {
229 | if (prefixCount != 0) yield(getPrefix(0)!!)
230 | yieldAll(requiredInPathList)
231 | }
232 | throw DependencyException(s.firstOrNull { !findExecutable(it) } ?: return)
233 | }
234 |
235 | private fun findExecutable(name: String): Boolean {
236 | val endings = if ("Windows" in System.getProperty("os.name")) listOf("", ".cmd", ".exe", ".com", ".bat") else listOf("")
237 | return sequence {
238 | yield(null) // current directory
239 | yieldAll(System.getenv().filterKeys { it.equals("PATH", ignoreCase = true) }.values.map { it.split(File.pathSeparator) }.flatten())
240 | }.any { parent -> endings.any { ending -> canExecute(File(parent, name + ending)) } }
241 | }
242 |
243 | private fun canExecute(f: File): Boolean = f.exists() && !f.isDirectory && f.canExecute()
244 |
245 | fun Window.repack() {
246 | val oldWidth = width
247 | pack()
248 | val loc = location
249 | setLocation(loc.x + ((oldWidth - width) / 2), loc.y)
250 | }
251 |
252 | fun Piper.Config.updateEnabled(value: Boolean): Piper.Config {
253 | return Piper.Config.newBuilder()
254 | .addAllMacro (macroList .map { it.buildEnabled(value) })
255 | .addAllMenuItem (menuItemList .map { it.buildEnabled(value) })
256 | .addAllMessageViewer (messageViewerList .map { it.buildEnabled(value) })
257 | .addAllHttpListener (httpListenerList .map { it.buildEnabled(value) })
258 | .addAllCommentator (commentatorList .map { it.buildEnabled(value) })
259 | .addAllIntruderPayloadProcessor(intruderPayloadProcessorList.map { it.buildEnabled(value) })
260 | .addAllIntruderPayloadGenerator(intruderPayloadGeneratorList.map { it.buildEnabled(value) })
261 | .addAllHighlighter (highlighterList .map { it.buildEnabled(value) })
262 | .build()
263 | }
--------------------------------------------------------------------------------
/src/main/kotlin/burp/Serialization.kt:
--------------------------------------------------------------------------------
1 | package burp
2 |
3 | import com.google.protobuf.ByteString
4 | import org.snakeyaml.engine.v1.api.Load
5 | import org.snakeyaml.engine.v1.api.LoadSettingsBuilder
6 | import java.io.ByteArrayOutputStream
7 | import java.io.InputStream
8 | import java.lang.RuntimeException
9 | import java.util.zip.DeflaterOutputStream
10 | import java.util.zip.InflaterInputStream
11 |
12 | fun configFromYaml(value: String): Piper.Config {
13 | val ls = Load(LoadSettingsBuilder().build())
14 | val b = Piper.Config.newBuilder()!!
15 | with(ls.loadFromString(value) as Map>) {
16 | copyListOfStructured("messageViewers", b::addMessageViewer, ::messageViewerFromMap)
17 | copyListOfStructured("macros", b::addMacro, ::minimalToolFromMap)
18 | copyListOfStructured("menuItems", b::addMenuItem, UserActionToolFromMap)
19 | copyListOfStructured("httpListeners", b::addHttpListener, ::httpListenerFromMap)
20 | copyListOfStructured("commentators", b::addCommentator, ::commentatorFromMap)
21 | copyListOfStructured("intruderPayloadProcessors", b::addIntruderPayloadProcessor, ::minimalToolFromMap)
22 | copyListOfStructured("highlighters", b::addHighlighter, ::highlighterFromMap)
23 | copyListOfStructured("intruderPayloadGenerators", b::addIntruderPayloadGenerator, ::minimalToolFromMap)
24 | }
25 | return b.build()
26 | }
27 |
28 | fun highlighterFromMap(source: Map): Piper.Highlighter {
29 | val b = Piper.Highlighter.newBuilder()!!
30 | source.copyBooleanFlag("overwrite", b::setOverwrite)
31 | source.copyBooleanFlag("applyWithListener", b::setApplyWithListener)
32 | val c = source["color"]
33 | if (c != null) b.color = c.toString()
34 | return b.setCommon(minimalToolFromMap(source)).build()
35 | }
36 |
37 | fun commentatorFromMap(source: Map): Piper.Commentator {
38 | val b = Piper.Commentator.newBuilder()!!
39 | source.copyBooleanFlag("overwrite", b::setOverwrite)
40 | source.copyBooleanFlag("applyWithListener", b::setApplyWithListener)
41 | return b.setCommon(minimalToolFromMap(source)).build()
42 | }
43 |
44 | fun messageViewerFromMap(source: Map): Piper.MessageViewer {
45 | val b = Piper.MessageViewer.newBuilder()!!
46 | source.copyBooleanFlag("usesColors", b::setUsesColors)
47 | return b.setCommon(minimalToolFromMap(source)).build()
48 | }
49 |
50 | fun minimalToolFromMap(source: Map): Piper.MinimalTool {
51 | val b = Piper.MinimalTool.newBuilder()!!
52 | .setName(source.stringOrDie("name"))
53 | .setCmd(commandInvocationFromMap(source))
54 | val scope = source["scope"]
55 | if (scope != null && scope is String) {
56 | b.scope = enumFromString(scope, Piper.MinimalTool.Scope::class.java)
57 | }
58 | source.copyStructured("filter", b::setFilter, ::messageMatchFromMap)
59 | return b.build()
60 | }
61 |
62 | fun httpListenerFromMap(source: Map): Piper.HttpListener {
63 | val b = Piper.HttpListener.newBuilder()!!
64 | .setScope(enumFromString(source.stringOrDie("scope"),
65 | Piper.HttpListenerScope::class.java))
66 | val ss = source.stringSequence("tool", required = false)
67 | .map { enumFromString(it, BurpTool::class.java) }
68 | if (ss.isNotEmpty()) b.setToolSet(ss.toSet())
69 | source.copyBooleanFlag("ignoreOutput", b::setIgnoreOutput)
70 | val minimalToolMap = source.toMutableMap()
71 | minimalToolMap["scope"] = Piper.MinimalTool.Scope.REQUEST_RESPONSE.name
72 | return b.setCommon(minimalToolFromMap(minimalToolMap)).build()
73 | }
74 |
75 | fun commandInvocationFromMap(source: Map): Piper.CommandInvocation {
76 | val b = Piper.CommandInvocation.newBuilder()!!
77 | .addAllPrefix(source.stringSequence("prefix"))
78 | .addAllPostfix(source.stringSequence("postfix", required = false))
79 | .setInputMethod(enumFromString(source.stringOrDie("inputMethod"),
80 | Piper.CommandInvocation.InputMethod::class.java))
81 | .addAllRequiredInPath(source.stringSequence("requiredInPath", required = false))
82 | .addAllExitCode(source.intSequence("exitCode"))
83 | with(source) {
84 | copyBooleanFlag("passHeaders", b::setPassHeaders)
85 | copyStructured("stdout", b::setStdout, ::messageMatchFromMap)
86 | copyStructured("stderr", b::setStderr, ::messageMatchFromMap)
87 | }
88 | return b.build()
89 | }
90 |
91 | fun messageMatchFromMap(source: Map): Piper.MessageMatch {
92 | val b = Piper.MessageMatch.newBuilder()!!
93 | with(source) {
94 | copyBytes("prefix", b::setPrefix)
95 | copyBytes("postfix", b::setPostfix)
96 | copyBooleanFlag("negation", b::setNegation)
97 | copyBooleanFlag("inScope", b::setInScope)
98 | copyStructured("regex", b::setRegex, RegExpFromMap)
99 | copyStructured("header", b::setHeader, HeaderMatchFromMap)
100 | copyStructured("cmd", b::setCmd, ::commandInvocationFromMap)
101 | copyListOfStructured("andAlso", b::addAndAlso, ::messageMatchFromMap)
102 | copyListOfStructured("orElse", b::addOrElse, ::messageMatchFromMap)
103 | }
104 | return b.build()
105 | }
106 |
107 | object HeaderMatchFromMap : (Map) -> Piper.HeaderMatch {
108 | override fun invoke(source: Map): Piper.HeaderMatch {
109 | val b = Piper.HeaderMatch.newBuilder()!!
110 | .setHeader(source.stringOrDie("header"))
111 | source.copyStructured("regex", b::setRegex, RegExpFromMap)
112 | return b.build()
113 | }
114 | }
115 |
116 | object RegExpFromMap : (Map) -> Piper.RegularExpression {
117 | override fun invoke(source: Map): Piper.RegularExpression {
118 | val b = Piper.RegularExpression.newBuilder()!!
119 | .setPattern(source.stringOrDie("pattern"))
120 | val ss = source.stringSequence("flags", required = false)
121 | .map { enumFromString(it, RegExpFlag::class.java) }
122 | if (ss.isNotEmpty()) b.setFlagSet(ss.toSet())
123 | return b.build()
124 | }
125 | }
126 |
127 | object UserActionToolFromMap : (Map) -> Piper.UserActionTool {
128 | override fun invoke(source: Map): Piper.UserActionTool {
129 | val b = Piper.UserActionTool.newBuilder()!!
130 | .setCommon(minimalToolFromMap(source))
131 | source.copyBooleanFlag("hasGUI", b::setHasGUI)
132 | source.copyBooleanFlag("avoidPipe", b::setAvoidPipe)
133 | source.copyInt("minInputs", b::setMinInputs)
134 | source.copyInt("maxInputs", b::setMaxInputs)
135 | return b.build()
136 | }
137 | }
138 |
139 | fun Map.copyInt(key: String, setter: (Int) -> Any) {
140 | when (val value = this[key] ?: return) {
141 | is Int -> if (value != 0) setter(value)
142 | else -> throw RuntimeException("Invalid value for $key: $value")
143 | }
144 | }
145 |
146 | fun Map.copyStructured(key: String, setter: (E) -> Any, transform: (Map) -> E) {
147 | when (val value = this[key] ?: return) {
148 | is Map<*, *> -> setter(transform(value as Map))
149 | else -> throw RuntimeException("Invalid value for $key: $value")
150 | }
151 | }
152 |
153 | fun Map.copyListOfStructured(key: String, setter: (E) -> Any, transform: (Map) -> E) {
154 | when (val value = this[key] ?: return) {
155 | is List<*> -> value.forEach { setter(transform(it as Map)) }
156 | else -> throw RuntimeException("Invalid value for $key: $value")
157 | }
158 | }
159 |
160 | fun Map.copyBytes(key: String, setter: (ByteString) -> Any) {
161 | when (val value = this[key] ?: return) {
162 | is String -> setter(ByteString.copyFromUtf8(value))
163 | is ByteArray -> setter(ByteString.copyFrom(value))
164 | else -> throw RuntimeException("Invalid value for $key: $value")
165 | }
166 | }
167 |
168 | fun > enumFromString(value: String, cls: Class): E {
169 | try {
170 | val search = value.replace(' ', '_')
171 | return cls.enumConstants.first { it.name.equals(search, ignoreCase = true) }
172 | } catch (_: NoSuchElementException) {
173 | throw RuntimeException("Invalid value for enumerated type: $value")
174 | }
175 | }
176 |
177 | fun Map.stringOrDie(key: String): String {
178 | when (val value = this[key]) {
179 | null -> throw RuntimeException("Missing value for $key")
180 | is String -> return value
181 | else -> throw RuntimeException("Invalid value for $key: $value")
182 | }
183 | }
184 |
185 | fun Map.stringSequence(key: String, required: Boolean = true): Iterable {
186 | return when (val value = this[key]) {
187 | null -> if (required) throw RuntimeException("Missing list for $key") else return emptyList()
188 | is List<*> -> value.map {
189 | when (it) {
190 | null -> throw RuntimeException("Invalid item for $key")
191 | is String -> return@map it
192 | else -> throw RuntimeException("Invalid value for $key: $it")
193 | }
194 | }
195 | else -> throw RuntimeException("Invalid value for $key: $value")
196 | }
197 | }
198 |
199 | fun Map.intSequence(key: String): Iterable {
200 | return when (val value = this[key]) {
201 | null -> emptyList()
202 | is List<*> -> value.map {
203 | when (it) {
204 | null -> throw RuntimeException("Invalid item for $key")
205 | is Int -> return@map it
206 | else -> throw RuntimeException("Invalid value for $key: $it")
207 | }
208 | }
209 | else -> throw RuntimeException("Invalid value for $key: $value")
210 | }
211 | }
212 |
213 | fun Map.copyBooleanFlag(key: String, setter: (Boolean) -> Any) {
214 | val value = this[key]
215 | if (value != null && value is Boolean && value) setter(true)
216 | }
217 |
218 | fun pad4(value: ByteArray): ByteArray {
219 | val pad = (4 - value.size % 4).toByte()
220 | return value + pad.downTo(1).map { pad }.toByteArray()
221 | }
222 |
223 | @Suppress("SpellCheckingInspection")
224 | fun unpad4(value: ByteArray): ByteArray =
225 | value.dropLast(value.last().toInt()).toByteArray()
226 |
227 | fun compress(value: ByteArray): ByteArray {
228 | val bos = ByteArrayOutputStream()
229 | DeflaterOutputStream(bos).use { it.write(value) }
230 | return bos.toByteArray()
231 | }
232 |
233 | fun decompress(value: ByteArray): ByteArray =
234 | InflaterInputStream(value.inputStream()).use(InputStream::readBytes)
235 |
236 | fun Piper.Config.toSettings(): Map = mutableMapOf().apply {
237 | add("messageViewers", messageViewerList, Piper.MessageViewer::toMap)
238 | add("menuItems", menuItemList, Piper.UserActionTool::toMap)
239 | add("macros", macroList, Piper.MinimalTool::toMap)
240 | add("intruderPayloadProcessors", intruderPayloadProcessorList, Piper.MinimalTool::toMap)
241 | add("httpListeners", httpListenerList, Piper.HttpListener::toMap)
242 | add("commentators", commentatorList, Piper.Commentator::toMap)
243 | add("highlighters", highlighterList, Piper.Highlighter::toMap)
244 | add("intruderPayloadGenerators", intruderPayloadGeneratorList, Piper.MinimalTool::toMap)
245 | }
246 |
247 | fun MutableMap.add(key: String, value: List, transform: (E) -> Any) {
248 | if (value.isNotEmpty()) this[key] = value.map(transform)
249 | }
250 |
251 | fun MutableMap.add(key: String, value: ByteString?) {
252 | if (value == null || value.isEmpty) return
253 | this[key] = if (value.isValidUtf8) value.toStringUtf8() else value.toByteArray()
254 | }
255 |
256 | fun Piper.MessageViewer.toMap(): Map =
257 | if (this.usesColors) this.common.toMap() + ("usesColors" to true) else this.common.toMap()
258 |
259 | fun Piper.UserActionTool.toMap(): Map {
260 | val m = this.common.toMap()
261 | if (this.hasGUI) m["hasGUI"] = true
262 | if (this.avoidPipe) m["avoidPipe"] = true
263 | if (this.minInputs != 0) m["minInputs"] = this.minInputs
264 | if (this.maxInputs != 0) m["maxInputs"] = this.maxInputs
265 | return m
266 | }
267 |
268 | fun Piper.HttpListener.toMap(): Map {
269 | val m = this.common.toMap()
270 | if (this.tool != 0) m["tool"] = this.toolSet.toSortedStringList()
271 | m["scope"] = this.scope.name.toLowerCase()
272 | if (this.ignoreOutput) m["ignoreOutput"] = true
273 | return m
274 | }
275 |
276 | fun Piper.Commentator.toMap(): Map {
277 | val m = this.common.toMap()
278 | if (this.overwrite) m["overwrite"] = true
279 | if (this.applyWithListener) m["applyWithListener"] = true
280 | return m
281 | }
282 |
283 | fun Piper.Highlighter.toMap(): Map {
284 | val m = this.common.toMap()
285 | if (this.overwrite) m["overwrite"] = true
286 | if (this.applyWithListener) m["applyWithListener"] = true
287 | if (!this.color.isNullOrEmpty()) m["color"] = this.color
288 | return m
289 | }
290 |
291 | fun Piper.MinimalTool.toMap(): MutableMap {
292 | val m = this.cmd.toMap()
293 | m["name"] = this.name!!
294 | if (this.hasFilter()) m["filter"] = this.filter.toMap()
295 | if (this.scope != Piper.MinimalTool.Scope.REQUEST_RESPONSE) m["scope"] = this.scope.name.toLowerCase()
296 | return m
297 | }
298 |
299 | fun Piper.MessageMatch.toMap(): Map {
300 | val m = mutableMapOf()
301 | m.add("prefix", this.prefix)
302 | m.add("postfix", this.postfix)
303 | if (this.hasRegex()) m["regex"] = this.regex.toMap()
304 | if (this.hasHeader()) m["header"] = this.header.toMap()
305 | if (this.hasCmd()) m["cmd"] = this.cmd.toMap()
306 | if (this.negation) m["negation"] = true
307 | if (this.inScope) m["inScope"] = true
308 | if (this.orElseCount > 0) m["orElse"] = this.orElseList.map(Piper.MessageMatch::toMap)
309 | if (this.andAlsoCount > 0) m["andAlso"] = this.andAlsoList.map(Piper.MessageMatch::toMap)
310 | return m
311 | }
312 |
313 | fun Piper.CommandInvocation.toMap(): MutableMap {
314 | val m = mutableMapOf()
315 | m["prefix"] = this.prefixList
316 | if (this.postfixCount > 0) m["postfix"] = this.postfixList
317 | m["inputMethod"] = this.inputMethod.name.toLowerCase()
318 | if (this.passHeaders) m["passHeaders"] = true
319 | if (this.requiredInPathCount > 0) m["requiredInPath"] = this.requiredInPathList
320 | if (this.exitCodeCount > 0) m["exitCode"] = this.exitCodeList
321 | if (this.hasStdout()) m["stdout"] = this.stdout.toMap()
322 | if (this.hasStderr()) m["stderr"] = this.stderr.toMap()
323 | return m
324 | }
325 |
326 | fun Piper.RegularExpression.toMap(): Map {
327 | val m = mutableMapOf("pattern" to this.pattern!!)
328 | if (this.flags != 0) m["flags"] = this.flagSet.toSortedStringList()
329 | return m
330 | }
331 |
332 | fun Set.toSortedStringList() = this.asSequence().map { it.toString() }.sorted().toList()
333 |
334 | fun Piper.HeaderMatch.toMap() = mapOf(("header" to this.header), ("regex" to this.regex.toMap()))
--------------------------------------------------------------------------------
/LICENSE.md:
--------------------------------------------------------------------------------
1 | ### GNU GENERAL PUBLIC LICENSE
2 |
3 | Version 3, 29 June 2007
4 |
5 | Copyright (C) 2007 Free Software Foundation, Inc.
6 |
7 |
8 | Everyone is permitted to copy and distribute verbatim copies of this
9 | license document, but changing it is not allowed.
10 |
11 | ### Preamble
12 |
13 | The GNU General Public License is a free, copyleft license for
14 | software and other kinds of works.
15 |
16 | The licenses for most software and other practical works are designed
17 | to take away your freedom to share and change the works. By contrast,
18 | the GNU General Public License is intended to guarantee your freedom
19 | to share and change all versions of a program--to make sure it remains
20 | free software for all its users. We, the Free Software Foundation, use
21 | the GNU General Public License for most of our software; it applies
22 | also to any other work released this way by its authors. You can apply
23 | it to your programs, too.
24 |
25 | When we speak of free software, we are referring to freedom, not
26 | price. Our General Public Licenses are designed to make sure that you
27 | have the freedom to distribute copies of free software (and charge for
28 | them if you wish), that you receive source code or can get it if you
29 | want it, that you can change the software or use pieces of it in new
30 | free programs, and that you know you can do these things.
31 |
32 | To protect your rights, we need to prevent others from denying you
33 | these rights or asking you to surrender the rights. Therefore, you
34 | have certain responsibilities if you distribute copies of the
35 | software, or if you modify it: responsibilities to respect the freedom
36 | of others.
37 |
38 | For example, if you distribute copies of such a program, whether
39 | gratis or for a fee, you must pass on to the recipients the same
40 | freedoms that you received. You must make sure that they, too, receive
41 | or can get the source code. And you must show them these terms so they
42 | know their rights.
43 |
44 | Developers that use the GNU GPL protect your rights with two steps:
45 | (1) assert copyright on the software, and (2) offer you this License
46 | giving you legal permission to copy, distribute and/or modify it.
47 |
48 | For the developers' and authors' protection, the GPL clearly explains
49 | that there is no warranty for this free software. For both users' and
50 | authors' sake, the GPL requires that modified versions be marked as
51 | changed, so that their problems will not be attributed erroneously to
52 | authors of previous versions.
53 |
54 | Some devices are designed to deny users access to install or run
55 | modified versions of the software inside them, although the
56 | manufacturer can do so. This is fundamentally incompatible with the
57 | aim of protecting users' freedom to change the software. The
58 | systematic pattern of such abuse occurs in the area of products for
59 | individuals to use, which is precisely where it is most unacceptable.
60 | Therefore, we have designed this version of the GPL to prohibit the
61 | practice for those products. If such problems arise substantially in
62 | other domains, we stand ready to extend this provision to those
63 | domains in future versions of the GPL, as needed to protect the
64 | freedom of users.
65 |
66 | Finally, every program is threatened constantly by software patents.
67 | States should not allow patents to restrict development and use of
68 | software on general-purpose computers, but in those that do, we wish
69 | to avoid the special danger that patents applied to a free program
70 | could make it effectively proprietary. To prevent this, the GPL
71 | assures that patents cannot be used to render the program non-free.
72 |
73 | The precise terms and conditions for copying, distribution and
74 | modification follow.
75 |
76 | ### TERMS AND CONDITIONS
77 |
78 | #### 0. Definitions.
79 |
80 | "This License" refers to version 3 of the GNU General Public License.
81 |
82 | "Copyright" also means copyright-like laws that apply to other kinds
83 | of works, such as semiconductor masks.
84 |
85 | "The Program" refers to any copyrightable work licensed under this
86 | License. Each licensee is addressed as "you". "Licensees" and
87 | "recipients" may be individuals or organizations.
88 |
89 | To "modify" a work means to copy from or adapt all or part of the work
90 | in a fashion requiring copyright permission, other than the making of
91 | an exact copy. The resulting work is called a "modified version" of
92 | the earlier work or a work "based on" the earlier work.
93 |
94 | A "covered work" means either the unmodified Program or a work based
95 | on the Program.
96 |
97 | To "propagate" a work means to do anything with it that, without
98 | permission, would make you directly or secondarily liable for
99 | infringement under applicable copyright law, except executing it on a
100 | computer or modifying a private copy. Propagation includes copying,
101 | distribution (with or without modification), making available to the
102 | public, and in some countries other activities as well.
103 |
104 | To "convey" a work means any kind of propagation that enables other
105 | parties to make or receive copies. Mere interaction with a user
106 | through a computer network, with no transfer of a copy, is not
107 | conveying.
108 |
109 | An interactive user interface displays "Appropriate Legal Notices" to
110 | the extent that it includes a convenient and prominently visible
111 | feature that (1) displays an appropriate copyright notice, and (2)
112 | tells the user that there is no warranty for the work (except to the
113 | extent that warranties are provided), that licensees may convey the
114 | work under this License, and how to view a copy of this License. If
115 | the interface presents a list of user commands or options, such as a
116 | menu, a prominent item in the list meets this criterion.
117 |
118 | #### 1. Source Code.
119 |
120 | The "source code" for a work means the preferred form of the work for
121 | making modifications to it. "Object code" means any non-source form of
122 | a work.
123 |
124 | A "Standard Interface" means an interface that either is an official
125 | standard defined by a recognized standards body, or, in the case of
126 | interfaces specified for a particular programming language, one that
127 | is widely used among developers working in that language.
128 |
129 | The "System Libraries" of an executable work include anything, other
130 | than the work as a whole, that (a) is included in the normal form of
131 | packaging a Major Component, but which is not part of that Major
132 | Component, and (b) serves only to enable use of the work with that
133 | Major Component, or to implement a Standard Interface for which an
134 | implementation is available to the public in source code form. A
135 | "Major Component", in this context, means a major essential component
136 | (kernel, window system, and so on) of the specific operating system
137 | (if any) on which the executable work runs, or a compiler used to
138 | produce the work, or an object code interpreter used to run it.
139 |
140 | The "Corresponding Source" for a work in object code form means all
141 | the source code needed to generate, install, and (for an executable
142 | work) run the object code and to modify the work, including scripts to
143 | control those activities. However, it does not include the work's
144 | System Libraries, or general-purpose tools or generally available free
145 | programs which are used unmodified in performing those activities but
146 | which are not part of the work. For example, Corresponding Source
147 | includes interface definition files associated with source files for
148 | the work, and the source code for shared libraries and dynamically
149 | linked subprograms that the work is specifically designed to require,
150 | such as by intimate data communication or control flow between those
151 | subprograms and other parts of the work.
152 |
153 | The Corresponding Source need not include anything that users can
154 | regenerate automatically from other parts of the Corresponding Source.
155 |
156 | The Corresponding Source for a work in source code form is that same
157 | work.
158 |
159 | #### 2. Basic Permissions.
160 |
161 | All rights granted under this License are granted for the term of
162 | copyright on the Program, and are irrevocable provided the stated
163 | conditions are met. This License explicitly affirms your unlimited
164 | permission to run the unmodified Program. The output from running a
165 | covered work is covered by this License only if the output, given its
166 | content, constitutes a covered work. This License acknowledges your
167 | rights of fair use or other equivalent, as provided by copyright law.
168 |
169 | You may make, run and propagate covered works that you do not convey,
170 | without conditions so long as your license otherwise remains in force.
171 | You may convey covered works to others for the sole purpose of having
172 | them make modifications exclusively for you, or provide you with
173 | facilities for running those works, provided that you comply with the
174 | terms of this License in conveying all material for which you do not
175 | control copyright. Those thus making or running the covered works for
176 | you must do so exclusively on your behalf, under your direction and
177 | control, on terms that prohibit them from making any copies of your
178 | copyrighted material outside their relationship with you.
179 |
180 | Conveying under any other circumstances is permitted solely under the
181 | conditions stated below. Sublicensing is not allowed; section 10 makes
182 | it unnecessary.
183 |
184 | #### 3. Protecting Users' Legal Rights From Anti-Circumvention Law.
185 |
186 | No covered work shall be deemed part of an effective technological
187 | measure under any applicable law fulfilling obligations under article
188 | 11 of the WIPO copyright treaty adopted on 20 December 1996, or
189 | similar laws prohibiting or restricting circumvention of such
190 | measures.
191 |
192 | When you convey a covered work, you waive any legal power to forbid
193 | circumvention of technological measures to the extent such
194 | circumvention is effected by exercising rights under this License with
195 | respect to the covered work, and you disclaim any intention to limit
196 | operation or modification of the work as a means of enforcing, against
197 | the work's users, your or third parties' legal rights to forbid
198 | circumvention of technological measures.
199 |
200 | #### 4. Conveying Verbatim Copies.
201 |
202 | You may convey verbatim copies of the Program's source code as you
203 | receive it, in any medium, provided that you conspicuously and
204 | appropriately publish on each copy an appropriate copyright notice;
205 | keep intact all notices stating that this License and any
206 | non-permissive terms added in accord with section 7 apply to the code;
207 | keep intact all notices of the absence of any warranty; and give all
208 | recipients a copy of this License along with the Program.
209 |
210 | You may charge any price or no price for each copy that you convey,
211 | and you may offer support or warranty protection for a fee.
212 |
213 | #### 5. Conveying Modified Source Versions.
214 |
215 | You may convey a work based on the Program, or the modifications to
216 | produce it from the Program, in the form of source code under the
217 | terms of section 4, provided that you also meet all of these
218 | conditions:
219 |
220 | - a) The work must carry prominent notices stating that you modified
221 | it, and giving a relevant date.
222 | - b) The work must carry prominent notices stating that it is
223 | released under this License and any conditions added under
224 | section 7. This requirement modifies the requirement in section 4
225 | to "keep intact all notices".
226 | - c) You must license the entire work, as a whole, under this
227 | License to anyone who comes into possession of a copy. This
228 | License will therefore apply, along with any applicable section 7
229 | additional terms, to the whole of the work, and all its parts,
230 | regardless of how they are packaged. This License gives no
231 | permission to license the work in any other way, but it does not
232 | invalidate such permission if you have separately received it.
233 | - d) If the work has interactive user interfaces, each must display
234 | Appropriate Legal Notices; however, if the Program has interactive
235 | interfaces that do not display Appropriate Legal Notices, your
236 | work need not make them do so.
237 |
238 | A compilation of a covered work with other separate and independent
239 | works, which are not by their nature extensions of the covered work,
240 | and which are not combined with it such as to form a larger program,
241 | in or on a volume of a storage or distribution medium, is called an
242 | "aggregate" if the compilation and its resulting copyright are not
243 | used to limit the access or legal rights of the compilation's users
244 | beyond what the individual works permit. Inclusion of a covered work
245 | in an aggregate does not cause this License to apply to the other
246 | parts of the aggregate.
247 |
248 | #### 6. Conveying Non-Source Forms.
249 |
250 | You may convey a covered work in object code form under the terms of
251 | sections 4 and 5, provided that you also convey the machine-readable
252 | Corresponding Source under the terms of this License, in one of these
253 | ways:
254 |
255 | - a) Convey the object code in, or embodied in, a physical product
256 | (including a physical distribution medium), accompanied by the
257 | Corresponding Source fixed on a durable physical medium
258 | customarily used for software interchange.
259 | - b) Convey the object code in, or embodied in, a physical product
260 | (including a physical distribution medium), accompanied by a
261 | written offer, valid for at least three years and valid for as
262 | long as you offer spare parts or customer support for that product
263 | model, to give anyone who possesses the object code either (1) a
264 | copy of the Corresponding Source for all the software in the
265 | product that is covered by this License, on a durable physical
266 | medium customarily used for software interchange, for a price no
267 | more than your reasonable cost of physically performing this
268 | conveying of source, or (2) access to copy the Corresponding
269 | Source from a network server at no charge.
270 | - c) Convey individual copies of the object code with a copy of the
271 | written offer to provide the Corresponding Source. This
272 | alternative is allowed only occasionally and noncommercially, and
273 | only if you received the object code with such an offer, in accord
274 | with subsection 6b.
275 | - d) Convey the object code by offering access from a designated
276 | place (gratis or for a charge), and offer equivalent access to the
277 | Corresponding Source in the same way through the same place at no
278 | further charge. You need not require recipients to copy the
279 | Corresponding Source along with the object code. If the place to
280 | copy the object code is a network server, the Corresponding Source
281 | may be on a different server (operated by you or a third party)
282 | that supports equivalent copying facilities, provided you maintain
283 | clear directions next to the object code saying where to find the
284 | Corresponding Source. Regardless of what server hosts the
285 | Corresponding Source, you remain obligated to ensure that it is
286 | available for as long as needed to satisfy these requirements.
287 | - e) Convey the object code using peer-to-peer transmission,
288 | provided you inform other peers where the object code and
289 | Corresponding Source of the work are being offered to the general
290 | public at no charge under subsection 6d.
291 |
292 | A separable portion of the object code, whose source code is excluded
293 | from the Corresponding Source as a System Library, need not be
294 | included in conveying the object code work.
295 |
296 | A "User Product" is either (1) a "consumer product", which means any
297 | tangible personal property which is normally used for personal,
298 | family, or household purposes, or (2) anything designed or sold for
299 | incorporation into a dwelling. In determining whether a product is a
300 | consumer product, doubtful cases shall be resolved in favor of
301 | coverage. For a particular product received by a particular user,
302 | "normally used" refers to a typical or common use of that class of
303 | product, regardless of the status of the particular user or of the way
304 | in which the particular user actually uses, or expects or is expected
305 | to use, the product. A product is a consumer product regardless of
306 | whether the product has substantial commercial, industrial or
307 | non-consumer uses, unless such uses represent the only significant
308 | mode of use of the product.
309 |
310 | "Installation Information" for a User Product means any methods,
311 | procedures, authorization keys, or other information required to
312 | install and execute modified versions of a covered work in that User
313 | Product from a modified version of its Corresponding Source. The
314 | information must suffice to ensure that the continued functioning of
315 | the modified object code is in no case prevented or interfered with
316 | solely because modification has been made.
317 |
318 | If you convey an object code work under this section in, or with, or
319 | specifically for use in, a User Product, and the conveying occurs as
320 | part of a transaction in which the right of possession and use of the
321 | User Product is transferred to the recipient in perpetuity or for a
322 | fixed term (regardless of how the transaction is characterized), the
323 | Corresponding Source conveyed under this section must be accompanied
324 | by the Installation Information. But this requirement does not apply
325 | if neither you nor any third party retains the ability to install
326 | modified object code on the User Product (for example, the work has
327 | been installed in ROM).
328 |
329 | The requirement to provide Installation Information does not include a
330 | requirement to continue to provide support service, warranty, or
331 | updates for a work that has been modified or installed by the
332 | recipient, or for the User Product in which it has been modified or
333 | installed. Access to a network may be denied when the modification
334 | itself materially and adversely affects the operation of the network
335 | or violates the rules and protocols for communication across the
336 | network.
337 |
338 | Corresponding Source conveyed, and Installation Information provided,
339 | in accord with this section must be in a format that is publicly
340 | documented (and with an implementation available to the public in
341 | source code form), and must require no special password or key for
342 | unpacking, reading or copying.
343 |
344 | #### 7. Additional Terms.
345 |
346 | "Additional permissions" are terms that supplement the terms of this
347 | License by making exceptions from one or more of its conditions.
348 | Additional permissions that are applicable to the entire Program shall
349 | be treated as though they were included in this License, to the extent
350 | that they are valid under applicable law. If additional permissions
351 | apply only to part of the Program, that part may be used separately
352 | under those permissions, but the entire Program remains governed by
353 | this License without regard to the additional permissions.
354 |
355 | When you convey a copy of a covered work, you may at your option
356 | remove any additional permissions from that copy, or from any part of
357 | it. (Additional permissions may be written to require their own
358 | removal in certain cases when you modify the work.) You may place
359 | additional permissions on material, added by you to a covered work,
360 | for which you have or can give appropriate copyright permission.
361 |
362 | Notwithstanding any other provision of this License, for material you
363 | add to a covered work, you may (if authorized by the copyright holders
364 | of that material) supplement the terms of this License with terms:
365 |
366 | - a) Disclaiming warranty or limiting liability differently from the
367 | terms of sections 15 and 16 of this License; or
368 | - b) Requiring preservation of specified reasonable legal notices or
369 | author attributions in that material or in the Appropriate Legal
370 | Notices displayed by works containing it; or
371 | - c) Prohibiting misrepresentation of the origin of that material,
372 | or requiring that modified versions of such material be marked in
373 | reasonable ways as different from the original version; or
374 | - d) Limiting the use for publicity purposes of names of licensors
375 | or authors of the material; or
376 | - e) Declining to grant rights under trademark law for use of some
377 | trade names, trademarks, or service marks; or
378 | - f) Requiring indemnification of licensors and authors of that
379 | material by anyone who conveys the material (or modified versions
380 | of it) with contractual assumptions of liability to the recipient,
381 | for any liability that these contractual assumptions directly
382 | impose on those licensors and authors.
383 |
384 | All other non-permissive additional terms are considered "further
385 | restrictions" within the meaning of section 10. If the Program as you
386 | received it, or any part of it, contains a notice stating that it is
387 | governed by this License along with a term that is a further
388 | restriction, you may remove that term. If a license document contains
389 | a further restriction but permits relicensing or conveying under this
390 | License, you may add to a covered work material governed by the terms
391 | of that license document, provided that the further restriction does
392 | not survive such relicensing or conveying.
393 |
394 | If you add terms to a covered work in accord with this section, you
395 | must place, in the relevant source files, a statement of the
396 | additional terms that apply to those files, or a notice indicating
397 | where to find the applicable terms.
398 |
399 | Additional terms, permissive or non-permissive, may be stated in the
400 | form of a separately written license, or stated as exceptions; the
401 | above requirements apply either way.
402 |
403 | #### 8. Termination.
404 |
405 | You may not propagate or modify a covered work except as expressly
406 | provided under this License. Any attempt otherwise to propagate or
407 | modify it is void, and will automatically terminate your rights under
408 | this License (including any patent licenses granted under the third
409 | paragraph of section 11).
410 |
411 | However, if you cease all violation of this License, then your license
412 | from a particular copyright holder is reinstated (a) provisionally,
413 | unless and until the copyright holder explicitly and finally
414 | terminates your license, and (b) permanently, if the copyright holder
415 | fails to notify you of the violation by some reasonable means prior to
416 | 60 days after the cessation.
417 |
418 | Moreover, your license from a particular copyright holder is
419 | reinstated permanently if the copyright holder notifies you of the
420 | violation by some reasonable means, this is the first time you have
421 | received notice of violation of this License (for any work) from that
422 | copyright holder, and you cure the violation prior to 30 days after
423 | your receipt of the notice.
424 |
425 | Termination of your rights under this section does not terminate the
426 | licenses of parties who have received copies or rights from you under
427 | this License. If your rights have been terminated and not permanently
428 | reinstated, you do not qualify to receive new licenses for the same
429 | material under section 10.
430 |
431 | #### 9. Acceptance Not Required for Having Copies.
432 |
433 | You are not required to accept this License in order to receive or run
434 | a copy of the Program. Ancillary propagation of a covered work
435 | occurring solely as a consequence of using peer-to-peer transmission
436 | to receive a copy likewise does not require acceptance. However,
437 | nothing other than this License grants you permission to propagate or
438 | modify any covered work. These actions infringe copyright if you do
439 | not accept this License. Therefore, by modifying or propagating a
440 | covered work, you indicate your acceptance of this License to do so.
441 |
442 | #### 10. Automatic Licensing of Downstream Recipients.
443 |
444 | Each time you convey a covered work, the recipient automatically
445 | receives a license from the original licensors, to run, modify and
446 | propagate that work, subject to this License. You are not responsible
447 | for enforcing compliance by third parties with this License.
448 |
449 | An "entity transaction" is a transaction transferring control of an
450 | organization, or substantially all assets of one, or subdividing an
451 | organization, or merging organizations. If propagation of a covered
452 | work results from an entity transaction, each party to that
453 | transaction who receives a copy of the work also receives whatever
454 | licenses to the work the party's predecessor in interest had or could
455 | give under the previous paragraph, plus a right to possession of the
456 | Corresponding Source of the work from the predecessor in interest, if
457 | the predecessor has it or can get it with reasonable efforts.
458 |
459 | You may not impose any further restrictions on the exercise of the
460 | rights granted or affirmed under this License. For example, you may
461 | not impose a license fee, royalty, or other charge for exercise of
462 | rights granted under this License, and you may not initiate litigation
463 | (including a cross-claim or counterclaim in a lawsuit) alleging that
464 | any patent claim is infringed by making, using, selling, offering for
465 | sale, or importing the Program or any portion of it.
466 |
467 | #### 11. Patents.
468 |
469 | A "contributor" is a copyright holder who authorizes use under this
470 | License of the Program or a work on which the Program is based. The
471 | work thus licensed is called the contributor's "contributor version".
472 |
473 | A contributor's "essential patent claims" are all patent claims owned
474 | or controlled by the contributor, whether already acquired or
475 | hereafter acquired, that would be infringed by some manner, permitted
476 | by this License, of making, using, or selling its contributor version,
477 | but do not include claims that would be infringed only as a
478 | consequence of further modification of the contributor version. For
479 | purposes of this definition, "control" includes the right to grant
480 | patent sublicenses in a manner consistent with the requirements of
481 | this License.
482 |
483 | Each contributor grants you a non-exclusive, worldwide, royalty-free
484 | patent license under the contributor's essential patent claims, to
485 | make, use, sell, offer for sale, import and otherwise run, modify and
486 | propagate the contents of its contributor version.
487 |
488 | In the following three paragraphs, a "patent license" is any express
489 | agreement or commitment, however denominated, not to enforce a patent
490 | (such as an express permission to practice a patent or covenant not to
491 | sue for patent infringement). To "grant" such a patent license to a
492 | party means to make such an agreement or commitment not to enforce a
493 | patent against the party.
494 |
495 | If you convey a covered work, knowingly relying on a patent license,
496 | and the Corresponding Source of the work is not available for anyone
497 | to copy, free of charge and under the terms of this License, through a
498 | publicly available network server or other readily accessible means,
499 | then you must either (1) cause the Corresponding Source to be so
500 | available, or (2) arrange to deprive yourself of the benefit of the
501 | patent license for this particular work, or (3) arrange, in a manner
502 | consistent with the requirements of this License, to extend the patent
503 | license to downstream recipients. "Knowingly relying" means you have
504 | actual knowledge that, but for the patent license, your conveying the
505 | covered work in a country, or your recipient's use of the covered work
506 | in a country, would infringe one or more identifiable patents in that
507 | country that you have reason to believe are valid.
508 |
509 | If, pursuant to or in connection with a single transaction or
510 | arrangement, you convey, or propagate by procuring conveyance of, a
511 | covered work, and grant a patent license to some of the parties
512 | receiving the covered work authorizing them to use, propagate, modify
513 | or convey a specific copy of the covered work, then the patent license
514 | you grant is automatically extended to all recipients of the covered
515 | work and works based on it.
516 |
517 | A patent license is "discriminatory" if it does not include within the
518 | scope of its coverage, prohibits the exercise of, or is conditioned on
519 | the non-exercise of one or more of the rights that are specifically
520 | granted under this License. You may not convey a covered work if you
521 | are a party to an arrangement with a third party that is in the
522 | business of distributing software, under which you make payment to the
523 | third party based on the extent of your activity of conveying the
524 | work, and under which the third party grants, to any of the parties
525 | who would receive the covered work from you, a discriminatory patent
526 | license (a) in connection with copies of the covered work conveyed by
527 | you (or copies made from those copies), or (b) primarily for and in
528 | connection with specific products or compilations that contain the
529 | covered work, unless you entered into that arrangement, or that patent
530 | license was granted, prior to 28 March 2007.
531 |
532 | Nothing in this License shall be construed as excluding or limiting
533 | any implied license or other defenses to infringement that may
534 | otherwise be available to you under applicable patent law.
535 |
536 | #### 12. No Surrender of Others' Freedom.
537 |
538 | If conditions are imposed on you (whether by court order, agreement or
539 | otherwise) that contradict the conditions of this License, they do not
540 | excuse you from the conditions of this License. If you cannot convey a
541 | covered work so as to satisfy simultaneously your obligations under
542 | this License and any other pertinent obligations, then as a
543 | consequence you may not convey it at all. For example, if you agree to
544 | terms that obligate you to collect a royalty for further conveying
545 | from those to whom you convey the Program, the only way you could
546 | satisfy both those terms and this License would be to refrain entirely
547 | from conveying the Program.
548 |
549 | #### 13. Use with the GNU Affero General Public License.
550 |
551 | Notwithstanding any other provision of this License, you have
552 | permission to link or combine any covered work with a work licensed
553 | under version 3 of the GNU Affero General Public License into a single
554 | combined work, and to convey the resulting work. The terms of this
555 | License will continue to apply to the part which is the covered work,
556 | but the special requirements of the GNU Affero General Public License,
557 | section 13, concerning interaction through a network will apply to the
558 | combination as such.
559 |
560 | #### 14. Revised Versions of this License.
561 |
562 | The Free Software Foundation may publish revised and/or new versions
563 | of the GNU General Public License from time to time. Such new versions
564 | will be similar in spirit to the present version, but may differ in
565 | detail to address new problems or concerns.
566 |
567 | Each version is given a distinguishing version number. If the Program
568 | specifies that a certain numbered version of the GNU General Public
569 | License "or any later version" applies to it, you have the option of
570 | following the terms and conditions either of that numbered version or
571 | of any later version published by the Free Software Foundation. If the
572 | Program does not specify a version number of the GNU General Public
573 | License, you may choose any version ever published by the Free
574 | Software Foundation.
575 |
576 | If the Program specifies that a proxy can decide which future versions
577 | of the GNU General Public License can be used, that proxy's public
578 | statement of acceptance of a version permanently authorizes you to
579 | choose that version for the Program.
580 |
581 | Later license versions may give you additional or different
582 | permissions. However, no additional obligations are imposed on any
583 | author or copyright holder as a result of your choosing to follow a
584 | later version.
585 |
586 | #### 15. Disclaimer of Warranty.
587 |
588 | THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY
589 | APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT
590 | HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT
591 | WARRANTY OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT
592 | LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
593 | A PARTICULAR PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND
594 | PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE PROGRAM PROVE
595 | DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, REPAIR OR
596 | CORRECTION.
597 |
598 | #### 16. Limitation of Liability.
599 |
600 | IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
601 | WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR
602 | CONVEYS THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES,
603 | INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES
604 | ARISING OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT
605 | NOT LIMITED TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR
606 | LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM
607 | TO OPERATE WITH ANY OTHER PROGRAMS), EVEN IF SUCH HOLDER OR OTHER
608 | PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES.
609 |
610 | #### 17. Interpretation of Sections 15 and 16.
611 |
612 | If the disclaimer of warranty and limitation of liability provided
613 | above cannot be given local legal effect according to their terms,
614 | reviewing courts shall apply local law that most closely approximates
615 | an absolute waiver of all civil liability in connection with the
616 | Program, unless a warranty or assumption of liability accompanies a
617 | copy of the Program in return for a fee.
618 |
619 | END OF TERMS AND CONDITIONS
620 |
621 | ### How to Apply These Terms to Your New Programs
622 |
623 | If you develop a new program, and you want it to be of the greatest
624 | possible use to the public, the best way to achieve this is to make it
625 | free software which everyone can redistribute and change under these
626 | terms.
627 |
628 | To do so, attach the following notices to the program. It is safest to
629 | attach them to the start of each source file to most effectively state
630 | the exclusion of warranty; and each file should have at least the
631 | "copyright" line and a pointer to where the full notice is found.
632 |
633 |
634 | Copyright (C)
635 |
636 | This program is free software: you can redistribute it and/or modify
637 | it under the terms of the GNU General Public License as published by
638 | the Free Software Foundation, either version 3 of the License, or
639 | (at your option) any later version.
640 |
641 | This program is distributed in the hope that it will be useful,
642 | but WITHOUT ANY WARRANTY; without even the implied warranty of
643 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
644 | GNU General Public License for more details.
645 |
646 | You should have received a copy of the GNU General Public License
647 | along with this program. If not, see .
648 |
649 | Also add information on how to contact you by electronic and paper
650 | mail.
651 |
652 | If the program does terminal interaction, make it output a short
653 | notice like this when it starts in an interactive mode:
654 |
655 | Copyright (C)
656 | This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
657 | This is free software, and you are welcome to redistribute it
658 | under certain conditions; type `show c' for details.
659 |
660 | The hypothetical commands \`show w' and \`show c' should show the
661 | appropriate parts of the General Public License. Of course, your
662 | program's commands might be different; for a GUI interface, you would
663 | use an "about box".
664 |
665 | You should also get your employer (if you work as a programmer) or
666 | school, if any, to sign a "copyright disclaimer" for the program, if
667 | necessary. For more information on this, and how to apply and follow
668 | the GNU GPL, see .
669 |
670 | The GNU General Public License does not permit incorporating your
671 | program into proprietary programs. If your program is a subroutine
672 | library, you may consider it more useful to permit linking proprietary
673 | applications with the library. If this is what you want to do, use the
674 | GNU Lesser General Public License instead of this License. But first,
675 | please read .
676 |
--------------------------------------------------------------------------------
/src/main/kotlin/burp/BurpExtender.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * This file is part of Piper for Burp Suite (https://github.com/silentsignal/burp-piper)
3 | * Copyright (c) 2018 Andras Veres-Szentkiralyi
4 | *
5 | * This program is free software: you can redistribute it and/or modify
6 | * it under the terms of the GNU General Public License as published by
7 | * the Free Software Foundation, version 3 of the License, or
8 | * (at your option) any later version.
9 | *
10 | * This program is distributed in the hope that it will be useful,
11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of
12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 | * GNU General Public License for more details.
14 | *
15 | * You should have received a copy of the GNU General Public License
16 | * along with this program. If not, see .
17 | */
18 |
19 | package burp
20 |
21 | import com.redpois0n.terminal.JTerminal
22 | import org.zeromq.codec.Z85
23 | import java.awt.*
24 | import java.awt.event.MouseEvent
25 | import java.awt.event.MouseListener
26 | import java.beans.PropertyChangeListener
27 | import java.beans.PropertyChangeSupport
28 | import java.io.BufferedReader
29 | import java.io.File
30 | import java.io.PrintStream
31 | import java.net.URL
32 | import java.time.LocalDateTime
33 | import java.time.format.DateTimeFormatter
34 | import java.util.*
35 | import javax.swing.*
36 | import javax.swing.event.ListDataEvent
37 | import javax.swing.event.ListDataListener
38 | import javax.swing.event.ListSelectionEvent
39 | import javax.swing.event.ListSelectionListener
40 | import javax.swing.filechooser.FileNameExtensionFilter
41 | import kotlin.concurrent.thread
42 |
43 |
44 | const val NAME = "Piper"
45 | const val EXTENSION_SETTINGS_KEY = "settings"
46 | const val CONFIG_ENV_VAR = "PIPER_CONFIG"
47 |
48 | data class MessageInfo(val content: ByteArray, val text: String, val headers: List?, val url: URL?, val hrr: IHttpRequestResponse? = null) {
49 | val asContentExtensionPair: Pair get() {
50 | return content to fileExtension
51 | }
52 |
53 | private val fileExtension: String? get() {
54 | if (url != null) {
55 | val match = Regex("\\.[a-z0-9]$", RegexOption.IGNORE_CASE).find(url.path)
56 | if (match != null) {
57 | return match.groups[0]!!.value
58 | }
59 | }
60 | headers?.filter { it.startsWith("content-type: ", ignoreCase = true) }?.forEach {
61 | val parts = it.split(' ', ';')[1].split('/')
62 | val ext = mimeTypes[parts[0]]?.get(parts[1]) ?: return@forEach
63 | return ".$ext"
64 | }
65 | return null
66 | }
67 |
68 | // make sure noone tries to compare such objects since they have an array member
69 | override fun equals(other: Any?): Boolean { throw NotImplementedError() }
70 | override fun hashCode(): Int { throw NotImplementedError() }
71 |
72 | companion object {
73 | val mimeTypes: Map>
74 |
75 | init {
76 | val db = Piper.MimeTypes.parseFrom(BurpExtender::class.java.classLoader.getResourceAsStream("mime.pb"))
77 | mimeTypes = db.typeOrBuilderList.map { type ->
78 | type.name to type.subtypeList.map { subtype ->
79 | subtype.name to subtype.extension
80 | }.toMap()
81 | }.toMap()
82 | }
83 | }
84 | }
85 |
86 | class BurpExtender : IBurpExtender, ITab, ListDataListener, IHttpListener {
87 |
88 | private lateinit var callbacks: IBurpExtenderCallbacks
89 | private lateinit var helpers: IExtensionHelpers
90 | private lateinit var configModel: ConfigModel
91 | private val queue = Queue()
92 | private val tabs = JTabbedPane()
93 |
94 | override fun contentsChanged(p0: ListDataEvent?) = saveConfig()
95 | override fun intervalAdded(p0: ListDataEvent?) = saveConfig()
96 | override fun intervalRemoved(p0: ListDataEvent?) = saveConfig()
97 |
98 | private inner class MessageViewerManager : RegisteredToolManager(
99 | configModel.messageViewersModel, callbacks::removeMessageEditorTabFactory, callbacks::registerMessageEditorTabFactory) {
100 | override fun isModelItemEnabled(item: Piper.MessageViewer): Boolean = item.common.enabled
101 |
102 | override fun modelToBurp(modelItem: Piper.MessageViewer): IMessageEditorTabFactory = IMessageEditorTabFactory { _, _ ->
103 | if (modelItem.usesColors) TerminalEditor(modelItem, helpers, callbacks)
104 | else TextEditor(modelItem, helpers, callbacks)
105 | }
106 | }
107 |
108 | private inner class MacroManager : RegisteredToolManager(
109 | configModel.macrosModel, callbacks::removeSessionHandlingAction, callbacks::registerSessionHandlingAction) {
110 | override fun isModelItemEnabled(item: Piper.MinimalTool): Boolean = item.enabled
111 |
112 | override fun modelToBurp(modelItem: Piper.MinimalTool): ISessionHandlingAction = object : ISessionHandlingAction {
113 | override fun performAction(currentRequest: IHttpRequestResponse?, macroItems: Array?) {
114 | modelItem.pipeMessage(Collections.singletonList(RequestResponse.REQUEST), currentRequest ?: return)
115 | }
116 |
117 | override fun getActionName(): String = modelItem.name
118 | }
119 | }
120 |
121 | private inner class HttpListenerManager : RegisteredToolManager(
122 | configModel.httpListenersModel, callbacks::removeHttpListener, callbacks::registerHttpListener) {
123 | override fun isModelItemEnabled(item: Piper.HttpListener): Boolean = item.common.enabled
124 |
125 | override fun modelToBurp(modelItem: Piper.HttpListener): IHttpListener = IHttpListener { toolFlag, messageIsRequest, messageInfo ->
126 | if ((messageIsRequest xor (modelItem.scope == Piper.HttpListenerScope.REQUEST))
127 | || (modelItem.tool != 0 && (modelItem.tool and toolFlag == 0))) return@IHttpListener
128 | modelItem.common.pipeMessage(ConfigHttpListenerScope.fromHttpListenerScope(modelItem.scope).inputList, messageInfo, modelItem.ignoreOutput)
129 | }
130 | }
131 |
132 | private inner class IntruderPayloadProcessorManager : RegisteredToolManager(
133 | configModel.intruderPayloadProcessorsModel, callbacks::removeIntruderPayloadProcessor, callbacks::registerIntruderPayloadProcessor) {
134 | override fun isModelItemEnabled(item: Piper.MinimalTool): Boolean = item.enabled
135 |
136 | override fun modelToBurp(modelItem: Piper.MinimalTool): IIntruderPayloadProcessor = object : IIntruderPayloadProcessor {
137 | override fun processPayload(currentPayload: ByteArray, originalPayload: ByteArray, baseValue: ByteArray): ByteArray? =
138 | if (modelItem.hasFilter() && !modelItem.filter.matches(MessageInfo(currentPayload, helpers.bytesToString(currentPayload),
139 | headers = null, url = null), helpers, callbacks)) null
140 | else getStdoutWithErrorHandling(modelItem.cmd.execute(currentPayload), modelItem)
141 |
142 | override fun getProcessorName(): String = modelItem.name
143 | }
144 | }
145 |
146 | private inner class IntruderPayloadGeneratorManager : RegisteredToolManager(
147 | configModel.intruderPayloadGeneratorsModel, callbacks::removeIntruderPayloadGeneratorFactory, callbacks::registerIntruderPayloadGeneratorFactory) {
148 | override fun isModelItemEnabled(item: Piper.MinimalTool): Boolean = item.enabled
149 |
150 | override fun modelToBurp(modelItem: Piper.MinimalTool): IIntruderPayloadGeneratorFactory = object : IIntruderPayloadGeneratorFactory {
151 | override fun createNewInstance(attack: IIntruderAttack?): IIntruderPayloadGenerator {
152 | return object : IIntruderPayloadGenerator {
153 | var process: Process? = null
154 | var reader: BufferedReader? = null
155 |
156 | override fun reset() {
157 | reader?.close()
158 | process?.destroy()
159 | process = null
160 | reader = null
161 | }
162 |
163 | override fun getNextPayload(baseValue: ByteArray?): ByteArray =
164 | stdout.readLine().toByteArray(charset = Charsets.ISO_8859_1)
165 |
166 | override fun hasMorePayloads(): Boolean {
167 | val p = process
168 | return p == null || p.isAlive || stdout.ready()
169 | }
170 |
171 | private val stdout: BufferedReader
172 | get() {
173 | val currentReader = reader
174 | return if (currentReader == null) {
175 | val p = modelItem.cmd.execute(ByteArray(0)).first
176 | process = p
177 | val newReader = p.inputStream.bufferedReader(charset = Charsets.ISO_8859_1)
178 | reader = newReader
179 | newReader
180 | } else currentReader
181 | }
182 | }
183 | }
184 |
185 | override fun getGeneratorName(): String = modelItem.name
186 | }
187 | }
188 |
189 | private abstract inner class RegisteredToolManager(private val model: DefaultListModel,
190 | private val remove: (B) -> Unit,
191 | private val add: (B) -> Unit) : ListDataListener {
192 |
193 | private val registeredInBurp: MutableList = model.map(this::modelToRegListItem).toMutableList()
194 |
195 | abstract fun isModelItemEnabled(item: M): Boolean
196 | abstract fun modelToBurp(modelItem: M): B
197 |
198 | private fun modelToRegListItem(modelItem: M): B? =
199 | if (isModelItemEnabled(modelItem)) modelToBurp(modelItem).apply(add) else null
200 |
201 | override fun contentsChanged(e: ListDataEvent) {
202 | for (i in e.index0 .. e.index1) {
203 | val currentRegistered = registeredInBurp[i]
204 | registeredInBurp[i] = modelToRegListItem(model[i])
205 | remove(currentRegistered ?: continue)
206 | }
207 | saveConfig()
208 | }
209 |
210 | override fun intervalAdded(e: ListDataEvent) {
211 | for (i in e.index0 .. e.index1) registeredInBurp.add(i, modelToRegListItem(model[i]))
212 | saveConfig()
213 | }
214 |
215 | override fun intervalRemoved(e: ListDataEvent) {
216 | for (i in e.index1 downTo e.index0) remove(registeredInBurp.removeAt(i) ?: continue)
217 | saveConfig()
218 | }
219 | }
220 |
221 | override fun registerExtenderCallbacks(callbacks: IBurpExtenderCallbacks) {
222 | this.callbacks = callbacks
223 | helpers = callbacks.helpers
224 | configModel = ConfigModel(loadConfig())
225 |
226 | configModel.menuItemsModel.addListDataListener(this) // Menu items are loaded on-demand, thus saving the config is enough
227 | configModel.commentatorsModel.addListDataListener(this) // Commentators are menu items as well, see above
228 | configModel.highlightersModel.addListDataListener(this) // Highlighters are menu items as well, see above
229 | configModel.messageViewersModel.addListDataListener(MessageViewerManager())
230 | configModel.macrosModel.addListDataListener(MacroManager())
231 | configModel.httpListenersModel.addListDataListener(HttpListenerManager())
232 | configModel.intruderPayloadProcessorsModel.addListDataListener(IntruderPayloadProcessorManager())
233 | configModel.intruderPayloadGeneratorsModel.addListDataListener(IntruderPayloadGeneratorManager())
234 |
235 | configModel.addPropertyChangeListener({ saveConfig() })
236 |
237 | callbacks.setExtensionName(NAME)
238 | callbacks.registerContextMenuFactory {
239 | val messages = it.selectedMessages
240 | if (messages.isNullOrEmpty()) return@registerContextMenuFactory emptyList()
241 | val topLevel = JMenu(NAME)
242 | val sb = it.selectionBounds
243 | val selectionContext = if (sb == null || sb.toSet().size < 2) null else it.invocationContext to sb
244 | generateContextMenu(messages.asList(), topLevel::add, selectionContext, includeCommentators = true)
245 | if (topLevel.subElements.isEmpty()) return@registerContextMenuFactory emptyList()
246 | return@registerContextMenuFactory Collections.singletonList(topLevel as JMenuItem)
247 | }
248 |
249 | populateTabs(configModel, null)
250 | callbacks.addSuiteTab(this)
251 | callbacks.registerHttpListener(this) // TODO add/remove based on actual demand w.r.t current config
252 | }
253 |
254 | override fun processHttpMessage(toolFlag: Int, messageIsRequest: Boolean, messageInfo: IHttpRequestResponse) {
255 | if (messageIsRequest || toolFlag != IBurpExtenderCallbacks.TOOL_PROXY) return
256 | val messageDetails = messagesToMap(Collections.singleton(messageInfo))
257 |
258 | configModel.enabledCommentators.filter(Piper.Commentator::getApplyWithListener).forEach { cfgItem ->
259 | messageDetails.filterApplicable(cfgItem.common).forEach { (_, md) ->
260 | performCommentator(cfgItem, md)
261 | }
262 | }
263 |
264 | configModel.enabledHighlighters.filter(Piper.Highlighter::getApplyWithListener).forEach { cfgItem ->
265 | messageDetails.filterApplicable(cfgItem.common).forEach { (_, md) ->
266 | performHighlighter(cfgItem, md)
267 | }
268 | }
269 | }
270 |
271 | private fun Piper.MinimalTool.pipeMessage(rrList: List, messageInfo: IHttpRequestResponse, ignoreOutput: Boolean = false) {
272 | require(rrList.isNotEmpty())
273 | val body = rrList.map { rr ->
274 | val bytes = rr.getMessage(messageInfo)!!
275 | val headers = rr.getHeaders(bytes, helpers)
276 | val bo = if (this.cmd.passHeaders) 0 else rr.getBodyOffset(bytes, helpers)
277 | val body = if (this.cmd.passHeaders) bytes else {
278 | if (bo < bytes.size - 1) {
279 | bytes.copyOfRange(bo, bytes.size)
280 | } else null // if the request has no body, passHeaders=false tools have no use for it
281 | }
282 | body to headers
283 | }
284 | val (lastBody, headers) = body.last()
285 | if (lastBody == null) return
286 | if (this.hasFilter() && !this.filter.matches(MessageInfo(lastBody, helpers.bytesToString(lastBody),
287 | headers, try { helpers.analyzeRequest(messageInfo).url } catch (_: Exception) { null }),
288 | helpers, callbacks)) return
289 | val input = body.mapNotNull(Pair>::first).toTypedArray()
290 | val replacement = getStdoutWithErrorHandling(this.cmd.execute(*input), this)
291 | if (!ignoreOutput) {
292 | rrList.last().setMessage(messageInfo,
293 | if (this.cmd.passHeaders) replacement else helpers.buildHttpMessage(headers, replacement))
294 | }
295 | }
296 |
297 | private fun getStdoutWithErrorHandling(executionResult: Pair>, tool: Piper.MinimalTool): ByteArray =
298 | executionResult.processOutput { process ->
299 | if (configModel.developer) {
300 | val stderr = process.errorStream.readBytes()
301 | if (stderr.isNotEmpty()) {
302 | val formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")
303 | val ts = LocalDateTime.now().format(formatter)
304 | callbacks.stderr.buffered().use {
305 | it.bufferedWriter().use { w ->
306 | w.newLine()
307 | w.write("${tool.name} called ${tool.cmd.commandLine} at $ts and stderr was not empty:")
308 | w.newLine()
309 | w.newLine()
310 | }
311 | it.write(stderr)
312 | }
313 | }
314 | }
315 | process.inputStream.readBytes()
316 | }
317 |
318 | private fun populateTabs(cfg: ConfigModel, parent: Component?) {
319 | val switchToCommentator = { tabs.selectedIndex = 4 }
320 |
321 | tabs.addTab("Message viewers", MessageViewerListEditor(cfg.messageViewersModel, parent,
322 | cfg.commentatorsModel, switchToCommentator))
323 | tabs.addTab("Context menu items", MinimalToolListEditor(cfg.menuItemsModel, parent,
324 | ::MenuItemDialog, Piper.UserActionTool::getDefaultInstance, UserActionToolFromMap, Piper.UserActionTool::toMap))
325 | tabs.addTab("Macros", MinimalToolListEditor(cfg.macrosModel, parent,
326 | ::MacroDialog, Piper.MinimalTool::getDefaultInstance, ::minimalToolFromMap, Piper.MinimalTool::toMap))
327 | tabs.addTab("HTTP listeners", MinimalToolListEditor(cfg.httpListenersModel, parent,
328 | ::HttpListenerDialog, Piper.HttpListener::getDefaultInstance, ::httpListenerFromMap, Piper.HttpListener::toMap))
329 | tabs.addTab("Commentators", MinimalToolListEditor(cfg.commentatorsModel, parent,
330 | ::CommentatorDialog, Piper.Commentator::getDefaultInstance, ::commentatorFromMap, Piper.Commentator::toMap))
331 | tabs.addTab("Intruder payload processors", MinimalToolListEditor(cfg.intruderPayloadProcessorsModel, parent,
332 | ::IntruderPayloadProcessorDialog, Piper.MinimalTool::getDefaultInstance, ::minimalToolFromMap, Piper.MinimalTool::toMap))
333 | tabs.addTab("Intruder payload generators", MinimalToolListEditor(cfg.intruderPayloadGeneratorsModel, parent,
334 | ::IntruderPayloadGeneratorDialog, Piper.MinimalTool::getDefaultInstance, ::minimalToolFromMap, Piper.MinimalTool::toMap))
335 | tabs.addTab("Highlighters", MinimalToolListEditor(cfg.highlightersModel, parent,
336 | ::HighlighterDialog, Piper.Highlighter::getDefaultInstance, ::highlighterFromMap, Piper.Highlighter::toMap))
337 | tabs.addTab("Queue", queue)
338 | tabs.addTab("Load/Save configuration", createLoadSaveUI(cfg, parent))
339 | tabs.addTab("Developer", createDeveloperUI(cfg))
340 | }
341 |
342 | private fun createDeveloperUI(cfg: ConfigModel): Component =
343 | JCheckBox("show user interface elements suited for developers").apply {
344 | isSelected = cfg.developer
345 | cfg.addPropertyChangeListener({ isSelected = cfg.developer })
346 | addChangeListener { cfg.developer = isSelected }
347 | }
348 |
349 | // ITab members
350 | override fun getTabCaption(): String = NAME
351 | override fun getUiComponent(): Component = tabs
352 |
353 | private data class MessageSource(val direction: RequestResponse, val region: Region) : Comparable {
354 | enum class Region(val includeHeaders: Boolean) {
355 | WHOLE_MESSAGE(true),
356 | HTTP_BODY(false),
357 | SELECTION(false);
358 | }
359 |
360 | override fun compareTo(other: MessageSource): Int =
361 | compareValuesBy(this, other, MessageSource::direction, MessageSource::region)
362 | }
363 |
364 | private fun generateContextMenu(messages: Collection, add: (Component) -> Component,
365 | selectionContext: Pair?, includeCommentators: Boolean) {
366 | val msize = messages.size
367 | val plural = if (msize == 1) "" else "s"
368 | val selectionMenu = mutableListOf()
369 |
370 | fun createSubMenu(msrc: MessageSource) : JMenu = JMenu("Process $msize ${msrc.direction.name.toLowerCase()}$plural")
371 |
372 | fun EnumMap.addMenuItemIfApplicable(menuItem: Piper.UserActionTool, mv: Piper.MessageViewer?, msrc: MessageSource,
373 | md: List) {
374 | val (first, second) = if (mv == null) (menuItem.common to null) else (mv.common to menuItem.common)
375 | if (!isToolApplicable(first, msrc, md, MessageInfoMatchStrategy.ALL)) return
376 | val mi = createMenuItem(first, second) { performMenuAction(menuItem, md, mv) }
377 | if (msrc.region == MessageSource.Region.SELECTION) {
378 | selectionMenu.add(mi)
379 | } else {
380 | this.getOrPut(msrc.direction) { createSubMenu(msrc) }.add(mi)
381 | }
382 | }
383 |
384 | val messageDetails = messagesToMap(messages, selectionContext)
385 | val categoryMenus = EnumMap(RequestResponse::class.java)
386 |
387 | for (cfgItem in configModel.enabledMenuItems) {
388 | // TODO check dependencies
389 | if ((cfgItem.maxInputs != 0 && cfgItem.maxInputs < msize) || cfgItem.minInputs > msize) continue
390 | for ((msrc, md) in messageDetails) {
391 | categoryMenus.addMenuItemIfApplicable(cfgItem, null, msrc, md)
392 | if (!cfgItem.common.cmd.passHeaders && !cfgItem.common.hasFilter() && !cfgItem.avoidPipe) {
393 | configModel.enabledMessageViewers.forEach { mv ->
394 | categoryMenus.addMenuItemIfApplicable(cfgItem, mv, msrc, md)
395 | }
396 | }
397 | }
398 | }
399 |
400 | fun addMessageAnnotatorMenuItems(source: List, common: (E) -> Piper.MinimalTool, action: (E, List) -> Unit) {
401 | val childCategoryMenus = EnumMap(RequestResponse::class.java)
402 |
403 | source.forEach { cfgItem ->
404 | messageDetails.filterApplicable(common(cfgItem)).forEach {(msrc, md) ->
405 | val childMenu = childCategoryMenus.getOrPut(msrc.direction) {
406 | categoryMenus[msrc.direction]?.apply { addSeparator() }
407 | ?: createSubMenu(msrc).apply { categoryMenus[msrc.direction] = this }
408 | }
409 | childMenu.add(createMenuItem(common(cfgItem), null) { action(cfgItem, md) })
410 | }
411 | }
412 | }
413 |
414 | if (includeCommentators) {
415 | addMessageAnnotatorMenuItems(configModel.enabledCommentators, Piper.Commentator::getCommon, ::performCommentator)
416 | addMessageAnnotatorMenuItems(configModel.enabledHighlighters, Piper.Highlighter::getCommon, ::performHighlighter)
417 | }
418 |
419 | categoryMenus.values.map(add)
420 | if (selectionMenu.isNotEmpty()) add(JMenu("Process selection").apply { selectionMenu.map(this::add) })
421 | add(JMenuItem("Add to queue").apply { addActionListener { queue.add(messages) } })
422 | }
423 |
424 | private fun messagesToMap(messages: Collection, selectionContext: Pair? = null): Map> {
425 | val messageDetails = TreeMap>()
426 | for (rr in RequestResponse.values()) {
427 | val httpMessages = ArrayList(messages.size)
428 | val httpBodies = ArrayList(messages.size)
429 | val selections = ArrayList(1)
430 | messages.forEach {
431 | val bytes = rr.getMessage(it) ?: return@forEach
432 | val headers = rr.getHeaders(bytes, helpers)
433 | val url = try { helpers.analyzeRequest(it).url } catch (_: Exception) { null }
434 | httpMessages.add(MessageInfo(bytes, helpers.bytesToString(bytes), headers, url, it))
435 | val bo = rr.getBodyOffset(bytes, helpers)
436 | if (bo < bytes.size - 1) {
437 | // if the request has no body, passHeaders=false actions have no use for it
438 | val body = bytes.copyOfRange(bo, bytes.size)
439 | httpBodies.add(MessageInfo(body, helpers.bytesToString(body), headers, url, it))
440 | }
441 | if (selectionContext != null) {
442 | val (context, bounds) = selectionContext
443 | if (context in rr.contexts) {
444 | val body = try {
445 | // handle utf-8 content
446 | bytes.decodeToString(throwOnInvalidSequence=true).substring(bounds[0], bounds[1]).encodeToByteArray()
447 | } catch (ex: java.nio.charset.MalformedInputException) {
448 | // Converting to utf-8 string failed.
449 | // Revert to plain byte extraction
450 | bytes.copyOfRange(bounds[0], bounds[1])
451 | } catch (ex: Exception) {
452 | // What happened here?
453 | ex.printStackTrace(PrintStream(callbacks.stderr))
454 | bytes.copyOfRange(bounds[0], bounds[1])
455 | }
456 | selections.add(MessageInfo(body, helpers.bytesToString(body), headers, url, it))
457 | }
458 | }
459 | }
460 | messageDetails[MessageSource(rr, MessageSource.Region.WHOLE_MESSAGE)] = httpMessages
461 | if (httpBodies.isNotEmpty()) {
462 | messageDetails[MessageSource(rr, MessageSource.Region.HTTP_BODY)] = httpBodies
463 | }
464 | if (selections.isNotEmpty()) {
465 | messageDetails[MessageSource(rr, MessageSource.Region.SELECTION)] = selections
466 | }
467 | }
468 | return messageDetails
469 | }
470 |
471 | private fun createMenuItem(tool: Piper.MinimalTool, pipe: Piper.MinimalTool?, action: () -> Unit) =
472 | JMenuItem(tool.name + (if (pipe == null) "" else " | ${pipe.name}")).apply { addActionListener { action() } }
473 |
474 | private fun Map>.filterApplicable(tool: Piper.MinimalTool): Map> = filter {
475 | (msrc, md) -> isToolApplicable(tool, msrc, md, MessageInfoMatchStrategy.ANY)
476 | }
477 |
478 | private fun isToolApplicable(tool: Piper.MinimalTool, msrc: MessageSource, md: List, mims: MessageInfoMatchStrategy) =
479 | tool.cmd.passHeaders == msrc.region.includeHeaders && tool.isInToolScope(msrc.direction.isRequest) && tool.canProcess(md, mims, helpers, callbacks)
480 |
481 | inner class Queue : JPanel(BorderLayout()), ListDataListener, ListSelectionListener, MouseListener {
482 |
483 | inner class HttpRequestResponse(original: IHttpRequestResponse) : IHttpRequestResponse {
484 |
485 | inner class HttpService(original: IHttpService) : IHttpService {
486 | private val host = original.host
487 | private val port = original.port
488 | private val protocol = original.protocol
489 |
490 | override fun getHost(): String = host
491 | override fun getPort(): Int = port
492 | override fun getProtocol(): String = protocol
493 | }
494 |
495 | private val comment = original.comment
496 | private val highlight = original.highlight
497 | private val httpService = HttpService(original.httpService)
498 | private val request = original.request.clone()
499 | private val response = original.response.clone()
500 |
501 | override fun getComment(): String = comment
502 | override fun getHighlight(): String = highlight
503 | override fun getHttpService(): IHttpService = httpService
504 | override fun getRequest(): ByteArray = request
505 | override fun getResponse(): ByteArray = response
506 |
507 | override fun setComment(comment: String?) {}
508 | override fun setHighlight(color: String?) {}
509 | override fun setHttpService(httpService: IHttpService?) {}
510 | override fun setRequest(message: ByteArray?) {}
511 | override fun setResponse(message: ByteArray?) {}
512 |
513 | override fun toString() = toHumanReadable(this)
514 | }
515 |
516 | private val model = DefaultListModel()
517 | private val pnToolbar = JPanel()
518 | private val listWidget = JList(model)
519 | private val btnProcess = JButton("Process")
520 |
521 | fun add(values: Iterable) = values.map(::HttpRequestResponse).forEach(model::addElement)
522 |
523 | private fun toHumanReadable(value: IHttpRequestResponse): String {
524 | val req = helpers.analyzeRequest(value)
525 | val resp = helpers.analyzeResponse(value.response)
526 | val size = value.response.size - resp.bodyOffset
527 | val plural = if (size == 1) "" else "s"
528 | return "${resp.statusCode} ${req.method} ${req.url} (response size = $size byte$plural)"
529 | }
530 |
531 | private fun addButtons() {
532 | btnProcess.addActionListener {
533 | val b = it.source as Component
534 | val loc = b.locationOnScreen
535 | showMenu(loc.x, loc.y + b.height)
536 | }
537 |
538 | listOf(createRemoveButton(listWidget, model), btnProcess).map(pnToolbar::add)
539 | }
540 |
541 | private fun showMenu(x: Int, y: Int) {
542 | val pm = JPopupMenu()
543 | generateContextMenu(listWidget.selectedValuesList, pm::add, selectionContext = null, includeCommentators = false)
544 | pm.show(this, 0, 0)
545 | pm.setLocation(x, y)
546 | }
547 |
548 | override fun mouseClicked(event: MouseEvent) {
549 | if (event.button == MouseEvent.BUTTON3) {
550 | showMenu(event.xOnScreen, event.yOnScreen)
551 | }
552 | }
553 |
554 | override fun mouseEntered(p0: MouseEvent?) {}
555 | override fun mouseExited(p0: MouseEvent?) {}
556 | override fun mousePressed(p0: MouseEvent?) {}
557 | override fun mouseReleased(p0: MouseEvent?) {}
558 |
559 | override fun valueChanged(p0: ListSelectionEvent?) { updateBtnEnableDisableState() }
560 | override fun contentsChanged(p0: ListDataEvent?) { updateBtnEnableDisableState() }
561 | override fun intervalAdded (p0: ListDataEvent?) { updateBtnEnableDisableState() }
562 | override fun intervalRemoved(p0: ListDataEvent?) { updateBtnEnableDisableState() }
563 |
564 | private fun updateBtnEnableDisableState() {
565 | btnProcess.isEnabled = !listWidget.isSelectionEmpty
566 | }
567 |
568 | init {
569 | listWidget.addListSelectionListener(this)
570 | listWidget.addMouseListener(this)
571 | model.addListDataListener(this)
572 |
573 | addButtons()
574 | updateBtnEnableDisableState()
575 | add(pnToolbar, BorderLayout.NORTH)
576 | add(JScrollPane(listWidget), BorderLayout.CENTER)
577 | }
578 | }
579 |
580 | private fun loadConfig(): Piper.Config {
581 | try {
582 | val env = System.getenv(CONFIG_ENV_VAR)
583 | if (env != null) {
584 | val fmt = if (env.endsWith(".yml") || env.endsWith(".yaml")){
585 | ConfigFormat.YAML
586 | } else {
587 | ConfigFormat.PROTOBUF
588 | }
589 | val configFile = File(env)
590 | return fmt.parse(configFile.readBytes()).updateEnabled(true)
591 | }
592 |
593 | val serialized = callbacks.loadExtensionSetting(EXTENSION_SETTINGS_KEY)
594 | if (serialized != null) {
595 | return Piper.Config.parseFrom(decompress(unpad4(Z85.Z85Decoder(serialized))))
596 | }
597 |
598 | throw Exception("Fallback to default config")
599 | } catch (e: Exception) {
600 | val cfgMod = loadDefaultConfig()
601 | saveConfig(cfgMod)
602 | return cfgMod
603 | }
604 | }
605 |
606 | private fun saveConfig(cfg: Piper.Config = configModel.serialize()) {
607 | val serialized = Z85.Z85Encoder(pad4(compress(cfg.toByteArray())))
608 | callbacks.saveExtensionSetting(EXTENSION_SETTINGS_KEY, serialized)
609 | }
610 |
611 | private fun performMenuAction(cfgItem: Piper.UserActionTool, messages: List,
612 | messageViewer: Piper.MessageViewer?) {
613 | thread {
614 | val (input, tools) = if (messageViewer == null) {
615 | messages.map(MessageInfo::asContentExtensionPair) to Collections.singletonList(cfgItem.common)
616 | } else {
617 | messages.map { msg ->
618 | messageViewer.common.cmd.execute(msg.asContentExtensionPair).processOutput { process ->
619 | process.inputStream.use { it.readBytes() }
620 | } to null
621 | } to listOf(messageViewer.common, cfgItem.common)
622 | }
623 | cfgItem.common.cmd.execute(*input.toTypedArray()).processOutput { process ->
624 | if (!cfgItem.hasGUI) handleGUI(process, tools)
625 | }
626 | }.start()
627 | }
628 |
629 | private fun performCommentator(cfgItem: Piper.Commentator, messages: List) {
630 | messages.forEach { mi ->
631 | val hrr = mi.hrr ?: return@forEach
632 | if ((hrr.comment.isNullOrEmpty() || cfgItem.overwrite) &&
633 | (!cfgItem.common.hasFilter() || cfgItem.common.filter.matches(mi, helpers, callbacks))) {
634 | val stdout = cfgItem.common.cmd.execute(mi.asContentExtensionPair).processOutput { process ->
635 | process.inputStream.readBytes()
636 | }
637 | hrr.comment = String(stdout, Charsets.UTF_8)
638 | }
639 | }
640 | }
641 |
642 | private fun performHighlighter(cfgItem: Piper.Highlighter, messages: List) {
643 | messages.forEach { mi ->
644 | val hrr = mi.hrr ?: return@forEach
645 | if ((hrr.highlight.isNullOrEmpty() || cfgItem.overwrite) &&
646 | (!cfgItem.common.hasFilter() || cfgItem.common.filter.matches(mi, helpers, callbacks)) &&
647 | cfgItem.common.cmd.matches(mi.content, helpers, callbacks)) {
648 | val h = Highlight.fromString(cfgItem.color) ?: return@forEach
649 | hrr.highlight = h.burpValue
650 | }
651 | }
652 | }
653 |
654 | companion object {
655 | @JvmStatic
656 | fun main (args: Array) {
657 | if (args.size > 2 && args[0] == "build-static") {
658 | val map = mutableMapOf>()
659 | File(args[1]).bufferedReader().use { input ->
660 | input.forEachLine { line ->
661 | if (line.startsWith('#')) return@forEachLine
662 | val parts = line.split('\t', ' ').filter(String::isNotEmpty)
663 | val type = parts[0].split('/')
664 | val subtypes = map.getOrPut(type[0]) { mutableListOf() }
665 | val ext = parts[1]
666 | subtypes.add(Piper.MimeTypes.Subtype.newBuilder().setName(type[1]).setExtension(ext).build())
667 | }
668 | }
669 | val mt = Piper.MimeTypes.newBuilder()
670 | map.forEach { (typeName, subtypes) ->
671 | mt.addType(Piper.MimeTypes.Type.newBuilder().setName(typeName).addAllSubtype(subtypes))
672 | }
673 | File(args[2]).writeBytes(mt.build().toByteArray())
674 | return
675 | }
676 | val be = BurpExtender()
677 | val cfg = loadDefaultConfig()
678 | val dialog = JDialog()
679 | be.populateTabs(ConfigModel(cfg), dialog)
680 | showModalDialog(900, 600, be.uiComponent, NAME, dialog, null)
681 | }
682 | }
683 | }
684 |
685 | class ConfigModel(config: Piper.Config = Piper.Config.getDefaultInstance()) {
686 | private val pcs = PropertyChangeSupport(this)
687 |
688 | val enabledMessageViewers get() = messageViewersModel.toIterable().filter { it.common.enabled }
689 | val enabledMenuItems get() = menuItemsModel.toIterable().filter { it.common.enabled }
690 | val enabledCommentators get() = commentatorsModel.toIterable().filter { it.common.enabled }
691 | val enabledHighlighters get() = highlightersModel.toIterable().filter { it.common.enabled }
692 |
693 | val macrosModel = DefaultListModel()
694 | val messageViewersModel = DefaultListModel()
695 | val menuItemsModel = DefaultListModel()
696 | val httpListenersModel = DefaultListModel()
697 | val commentatorsModel = DefaultListModel()
698 | val intruderPayloadProcessorsModel = DefaultListModel()
699 | val highlightersModel = DefaultListModel()
700 | val intruderPayloadGeneratorsModel = DefaultListModel()
701 |
702 | private var _developer = config.developer
703 | var developer: Boolean
704 | get() = _developer
705 | set(value) {
706 | val old = _developer
707 | _developer = value
708 | pcs.firePropertyChange("developer", old, value)
709 | }
710 |
711 | init { fillModels(config) }
712 |
713 | fun addPropertyChangeListener(listener: PropertyChangeListener) {
714 | pcs.addPropertyChangeListener(listener)
715 | }
716 |
717 | fun fillModels(config: Piper.Config) {
718 | fillDefaultModel(config.macroList, macrosModel)
719 | fillDefaultModel(config.messageViewerList, messageViewersModel)
720 | fillDefaultModel(config.menuItemList, menuItemsModel)
721 | fillDefaultModel(config.httpListenerList, httpListenersModel)
722 | fillDefaultModel(config.commentatorList, commentatorsModel)
723 | fillDefaultModel(config.intruderPayloadProcessorList, intruderPayloadProcessorsModel)
724 | fillDefaultModel(config.highlighterList, highlightersModel)
725 | fillDefaultModel(config.intruderPayloadGeneratorList, intruderPayloadGeneratorsModel)
726 | }
727 |
728 | fun serialize(): Piper.Config = Piper.Config.newBuilder()
729 | .addAllMacro(macrosModel.toIterable())
730 | .addAllMessageViewer(messageViewersModel.toIterable())
731 | .addAllMenuItem(menuItemsModel.toIterable())
732 | .addAllHttpListener(httpListenersModel.toIterable())
733 | .addAllCommentator(commentatorsModel.toIterable())
734 | .addAllIntruderPayloadProcessor(intruderPayloadProcessorsModel.toIterable())
735 | .addAllHighlighter(highlightersModel.toIterable())
736 | .addAllIntruderPayloadGenerator(intruderPayloadGeneratorsModel.toIterable())
737 | .setDeveloper(developer)
738 | .build()
739 | }
740 |
741 | private fun createLoadSaveUI(cfg: ConfigModel, parent: Component?): Component {
742 | return JPanel().apply {
743 | add(JButton("Load/restore default config").apply {
744 | addActionListener {
745 | if (JOptionPane.showConfirmDialog(parent,
746 | "This will overwrite your currently loaded configuration with the default one. Are you sure?",
747 | "Confirm restoring default configuration", JOptionPane.OK_CANCEL_OPTION) == JOptionPane.OK_OPTION) {
748 | cfg.fillModels(loadDefaultConfig())
749 | }
750 | }
751 | })
752 | add(JButton("Export to YAML file" ).apply { addActionListener { exportConfig(ConfigFormat.YAML, cfg, parent) } })
753 | add(JButton("Export to ProtoBuf file" ).apply { addActionListener { exportConfig(ConfigFormat.PROTOBUF, cfg, parent) } })
754 | add(JButton("Import from YAML file" ).apply { addActionListener { importConfig(ConfigFormat.YAML, cfg, parent) } })
755 | add(JButton("Import from ProtoBuf file").apply { addActionListener { importConfig(ConfigFormat.PROTOBUF, cfg, parent) } })
756 | }
757 | }
758 |
759 | private fun exportConfig(fmt: ConfigFormat, cfg: ConfigModel, parent: Component?) {
760 | val fc = JFileChooser()
761 | fc.fileFilter = FileNameExtensionFilter(fmt.name, fmt.fileExtension)
762 | if (fc.showSaveDialog(parent) == JFileChooser.APPROVE_OPTION) {
763 | fc.selectedFile.writeBytes(fmt.serialize(cfg.serialize()))
764 | }
765 | }
766 |
767 | private fun importConfig(fmt: ConfigFormat, cfg: ConfigModel, parent: Component?) {
768 | val fc = JFileChooser()
769 | fc.fileFilter = FileNameExtensionFilter(fmt.name, fmt.fileExtension)
770 | if (fc.showOpenDialog(parent) == JFileChooser.APPROVE_OPTION) {
771 | try {
772 | cfg.fillModels(fmt.parse(fc.selectedFile.readBytes()))
773 | } catch (e: Exception) {
774 | JOptionPane.showMessageDialog(parent, e.message, "Error while importing ${fc.selectedFile}", JOptionPane.ERROR_MESSAGE)
775 | }
776 | }
777 | }
778 |
779 | private fun loadDefaultConfig(): Piper.Config {
780 | // TODO use more efficient Protocol Buffers encoded version
781 | return configFromYaml(BurpExtender::class.java.classLoader
782 | .getResourceAsStream("defaults.yaml").reader().readText()).updateEnabled(true)
783 | }
784 |
785 | private fun handleGUI(process: Process, tools: List) {
786 | val terminal = JTerminal()
787 | val scrollPane = JScrollPane()
788 | scrollPane.setViewportView(terminal)
789 | val frame = JFrame()
790 | with(frame) {
791 | defaultCloseOperation = JFrame.DISPOSE_ON_CLOSE
792 | addKeyListener(terminal.keyListener)
793 | add(scrollPane)
794 | setSize(675, 300)
795 | title = tools.joinToString(separator = " | ", prefix = "$NAME - ", transform = Piper.MinimalTool::getName)
796 | isVisible = true
797 | }
798 |
799 | for (stream in arrayOf(process.inputStream, process.errorStream)) {
800 | thread {
801 | val reader = stream.bufferedReader()
802 | while (true) {
803 | val line = reader.readLine() ?: break
804 | terminal.append("$line\n")
805 | }
806 | }.start()
807 | }
808 | }
809 |
--------------------------------------------------------------------------------
/src/main/kotlin/burp/ConfigGUI.kt:
--------------------------------------------------------------------------------
1 | package burp
2 |
3 | import com.google.protobuf.ByteString
4 | import org.snakeyaml.engine.v1.api.Dump
5 | import org.snakeyaml.engine.v1.api.DumpSettingsBuilder
6 | import org.snakeyaml.engine.v1.api.Load
7 | import org.snakeyaml.engine.v1.api.LoadSettingsBuilder
8 | import java.awt.*
9 | import java.awt.datatransfer.*
10 | import java.awt.event.*
11 | import java.util.*
12 | import javax.swing.*
13 | import javax.swing.event.ListDataEvent
14 | import javax.swing.event.ListDataListener
15 | import javax.swing.event.ListSelectionEvent
16 | import javax.swing.event.ListSelectionListener
17 | import kotlin.math.max
18 |
19 | private fun minimalToolHumanReadableName(cfgItem: Piper.MinimalTool) = if (cfgItem.enabled) cfgItem.name else cfgItem.name + " [disabled]"
20 |
21 | const val TOGGLE_DEFAULT = "Toggle enabled"
22 |
23 | abstract class ListEditor(protected val model: DefaultListModel, protected val parent: Component?,
24 | caption: String?) : JPanel(BorderLayout()), ListDataListener, ListCellRenderer, ListSelectionListener {
25 | protected val pnToolbar = JPanel()
26 | protected val listWidget = JList(model)
27 | private val btnClone = JButton("Clone")
28 | private val cr = DefaultListCellRenderer()
29 |
30 | abstract fun editDialog(value: E): E?
31 | abstract fun addDialog(): E?
32 | abstract fun toHumanReadable(value: E): String
33 |
34 | private fun addButtons() {
35 | val btnAdd = JButton("Add")
36 | btnAdd.addActionListener {
37 | model.addElement(addDialog() ?: return@addActionListener)
38 | }
39 | btnClone.addActionListener {
40 | (listWidget.selectedValuesList.reversed().asSequence() zip listWidget.selectedIndices.reversed().asSequence()).forEach {(value, index) ->
41 | model.insertElementAt(value, index)
42 | }
43 | }
44 |
45 | listOf(btnAdd, createRemoveButton(listWidget, model), btnClone).map(pnToolbar::add)
46 | }
47 |
48 | override fun getListCellRendererComponent(list: JList?, value: E, index: Int, isSelected: Boolean, cellHasFocus: Boolean): Component {
49 | val c = cr.getListCellRendererComponent(list, value, index, isSelected, cellHasFocus)
50 | cr.text = toHumanReadable(value)
51 | return c
52 | }
53 |
54 | override fun valueChanged(p0: ListSelectionEvent?) { updateBtnEnableDisableState() }
55 | override fun contentsChanged(p0: ListDataEvent?) { updateBtnEnableDisableState() }
56 | override fun intervalAdded (p0: ListDataEvent?) { updateBtnEnableDisableState() }
57 | override fun intervalRemoved(p0: ListDataEvent?) { updateBtnEnableDisableState() }
58 |
59 | private fun updateCloneBtnState() {
60 | btnClone.isEnabled = !listWidget.isSelectionEmpty
61 | }
62 |
63 | open fun updateBtnEnableDisableState() {
64 | updateCloneBtnState()
65 | }
66 |
67 | init {
68 | listWidget.addDoubleClickListener {
69 | model[it] = editDialog(model[it]) ?: return@addDoubleClickListener
70 | }
71 | listWidget.cellRenderer = this
72 |
73 | listWidget.addListSelectionListener(this)
74 | model.addListDataListener(this)
75 |
76 | addButtons()
77 | updateCloneBtnState()
78 | if (caption == null) {
79 | add(pnToolbar, BorderLayout.PAGE_START)
80 | } else {
81 | add(pnToolbar, BorderLayout.SOUTH)
82 | add(JLabel(caption), BorderLayout.PAGE_START)
83 | }
84 | add(JScrollPane(listWidget), BorderLayout.CENTER)
85 | }
86 | }
87 |
88 | open class MinimalToolListEditor(model: DefaultListModel, parent: Component?, private val dialog: (E, Component?) -> MinimalToolDialog,
89 | private val default: () -> E, private val fromMap: (Map) -> E,
90 | private val toMap: (E) -> Map) : ListEditor(model, parent, null), ClipboardOwner {
91 |
92 | private val btnEnableDisable = JButton()
93 | private val btnCopy = JButton("Copy")
94 | private val btnPaste = JButton("Paste")
95 |
96 | override fun addDialog(): E? {
97 | val enabledDefault = dialog(default(), parent).buildEnabled(true)
98 | return dialog(enabledDefault, parent).showGUI()
99 | }
100 |
101 | override fun editDialog(value: E): E? = dialog(value, parent).showGUI()
102 | override fun toHumanReadable(value: E): String = dialog(value, parent).toHumanReadable()
103 |
104 | override fun updateBtnEnableDisableState() {
105 | super.updateBtnEnableDisableState()
106 | updateEnableDisableBtnState()
107 | }
108 |
109 | private fun updateEnableDisableBtnState() {
110 | val si = listWidget.selectedIndices
111 | val selectionNotEmpty = si.isNotEmpty()
112 | btnCopy.isEnabled = selectionNotEmpty
113 | btnEnableDisable.isEnabled = selectionNotEmpty
114 | val maxIndex = si.maxOrNull()
115 | btnEnableDisable.text = if (maxIndex == null || maxIndex >= model.size()) TOGGLE_DEFAULT else
116 | {
117 | val states = listWidget.selectedValuesList.map { dialog(it, parent).isToolEnabled() }.toSet()
118 | if (states.size == 1) (if (states.first()) "Disable" else "Enable") else TOGGLE_DEFAULT
119 | }
120 | }
121 |
122 | init {
123 | btnCopy.addActionListener {
124 | Toolkit.getDefaultToolkit().systemClipboard.setContents(StringSelection(
125 | Dump(DumpSettingsBuilder().build()).dumpToString(toMap(listWidget.selectedValue ?: return@addActionListener))), this)
126 | }
127 | btnPaste.addActionListener {
128 | val s = Toolkit.getDefaultToolkit().systemClipboard.getData(DataFlavor.stringFlavor) as? String ?: return@addActionListener
129 | val ls = Load(LoadSettingsBuilder().build())
130 | try {
131 | model.addElement(fromMap(ls.loadFromString(s) as Map))
132 | } catch (e: Exception) {
133 | JOptionPane.showMessageDialog(listWidget, e.message)
134 | }
135 | }
136 | btnEnableDisable.addActionListener {
137 | (listWidget.selectedValuesList.asSequence() zip listWidget.selectedIndices.asSequence()).forEach { (value, index) ->
138 | model[index] = dialog(value, parent).buildEnabled(!dialog(value, parent).isToolEnabled())
139 | }
140 | }
141 | listOf(btnEnableDisable, btnCopy, btnPaste).map(pnToolbar::add)
142 | updateEnableDisableBtnState()
143 | }
144 |
145 | override fun lostOwnership(p0: Clipboard?, p1: Transferable?) {} /* ClipboardOwner */
146 | }
147 |
148 | class MessageViewerListEditor(model: DefaultListModel, parent: Component?,
149 | private val commentatorModel: DefaultListModel,
150 | private val switchToCommentator: () -> Unit) :
151 | MinimalToolListEditor(model, parent, ::MessageViewerDialog,
152 | Piper.MessageViewer::getDefaultInstance, ::messageViewerFromMap, Piper.MessageViewer::toMap) {
153 |
154 | private val btnConvertToCommentator = JButton("Convert to commentator")
155 |
156 | override fun updateBtnEnableDisableState() {
157 | super.updateBtnEnableDisableState()
158 | updateEnableDisableBtnState()
159 | }
160 |
161 | private fun updateEnableDisableBtnState() {
162 | btnConvertToCommentator.isEnabled = !listWidget.isSelectionEmpty
163 | }
164 |
165 | init {
166 | btnConvertToCommentator.addActionListener {
167 | listWidget.selectedValuesList.forEach {
168 | commentatorModel.addElement(Piper.Commentator.newBuilder().setCommon(it.common).build())
169 | }
170 | switchToCommentator()
171 | }
172 | pnToolbar.add(btnConvertToCommentator)
173 | updateEnableDisableBtnState()
174 | }
175 | }
176 |
177 | fun JList.addDoubleClickListener(listener: (Int) -> Unit) {
178 | this.addMouseListener(object : MouseAdapter() {
179 | override fun mouseClicked(e: MouseEvent) {
180 | if (e.clickCount == 2) {
181 | listener(this@addDoubleClickListener.locationToIndex(e.point))
182 | }
183 | }
184 | })
185 | }
186 |
187 | class CancelClosingWindow : RuntimeException()
188 |
189 | class MinimalToolWidget(tool: Piper.MinimalTool, private val panel: Container, cs: GridBagConstraints, w: Window,
190 | showPassHeaders: Boolean, purpose: CommandInvocationPurpose, showScope: Boolean, showFilter: Boolean) {
191 | private val tfName = createLabeledTextField("Name: ", tool.name, panel, cs)
192 | private val lsScope: JComboBox? = if (showScope) createLabeledWidget("Can handle... ",
193 | JComboBox(ConfigMinimalToolScope.values()).apply { selectedItem = ConfigMinimalToolScope.fromScope(tool.scope) }, panel, cs) else null
194 | private val cbEnabled: JCheckBox
195 | private val cciw: CollapsedCommandInvocationWidget = CollapsedCommandInvocationWidget(w, cmd = tool.cmd, purpose = purpose, showPassHeaders = showPassHeaders)
196 | private val ccmw: CollapsedMessageMatchWidget = CollapsedMessageMatchWidget(w, mm = tool.filter, showHeaderMatch = true, caption = "Filter: ")
197 |
198 | fun toMinimalTool(): Piper.MinimalTool {
199 | if (tfName.text.isEmpty()) throw RuntimeException("Name cannot be empty.")
200 | val command = cciw.value ?: throw RuntimeException("Command must be specified")
201 | try {
202 | if (cbEnabled.isSelected) command.checkDependencies()
203 | } catch (c: DependencyException) {
204 | when (JOptionPane.showConfirmDialog(panel, "${c.message}\n\nAre you sure you want this enabled?")) {
205 | JOptionPane.NO_OPTION -> cbEnabled.isSelected = false
206 | JOptionPane.CANCEL_OPTION -> throw CancelClosingWindow()
207 | }
208 | }
209 |
210 | return Piper.MinimalTool.newBuilder().apply {
211 | name = tfName.text
212 | if (cbEnabled.isSelected) enabled = true
213 | if (ccmw.value != null) filter = ccmw.value
214 | if (lsScope != null) scope = (lsScope.selectedItem as ConfigMinimalToolScope).scope
215 | cmd = command
216 | }.build()
217 | }
218 |
219 | fun addFilterChangeListener(listener: ChangeListener) {
220 | ccmw.addChangeListener(listener)
221 | }
222 |
223 | init {
224 | if (showFilter) ccmw.buildGUI(panel, cs)
225 | cciw.buildGUI(panel, cs)
226 | cbEnabled = createFullWidthCheckBox("Enabled", tool.enabled, panel, cs)
227 | }
228 | }
229 |
230 | abstract class CollapsedWidget(private val w: Window, var value: E?, private val caption: String, val removable: Boolean) : ClipboardOwner {
231 | private val label = JLabel()
232 | private val pnToolbar = JPanel(FlowLayout(FlowLayout.LEFT))
233 | private val btnRemove = JButton("Remove")
234 | private val btnCopy = JButton("Copy")
235 | private val btnPaste = JButton("Paste")
236 | private val changeListeners = mutableListOf>()
237 |
238 | abstract fun editDialog(value: E, parent: Component): E?
239 | abstract fun toHumanReadable(): String
240 | abstract val asMap: Map?
241 | abstract val default: E
242 |
243 | abstract fun parseMap(map: Map): E
244 |
245 | private fun update() {
246 | label.text = toHumanReadable() + " "
247 | btnRemove.isEnabled = value != null
248 | btnCopy.isEnabled = value != null
249 | w.repack()
250 | changeListeners.forEach { it.valueChanged(value) }
251 | }
252 |
253 | fun addChangeListener(listener: ChangeListener) {
254 | changeListeners.add(listener)
255 | listener.valueChanged(value)
256 | }
257 |
258 | fun buildGUI(panel: Container, cs: GridBagConstraints) {
259 | val btnEditFilter = JButton("Edit...")
260 | btnEditFilter.addActionListener {
261 | value = editDialog(value ?: default, panel) ?: return@addActionListener
262 | update()
263 | }
264 |
265 | update()
266 | cs.gridwidth = 1
267 | cs.gridy++
268 |
269 | cs.gridx = 0 ; panel.add(JLabel(caption), cs)
270 | cs.gridx = 1 ; panel.add(label, cs)
271 | cs.gridwidth = 2
272 | cs.gridx = 2 ; panel.add(pnToolbar, cs)
273 |
274 | listOf(btnEditFilter, btnCopy, btnPaste).map(pnToolbar::add)
275 |
276 | if (removable) {
277 | pnToolbar.add(btnRemove)
278 | btnRemove.addActionListener {
279 | value = null
280 | update()
281 | }
282 | }
283 | }
284 |
285 | init {
286 | if (value == default) value = null
287 | btnCopy.addActionListener {
288 | Toolkit.getDefaultToolkit().systemClipboard.setContents(StringSelection(
289 | Dump(DumpSettingsBuilder().build()).dumpToString(asMap ?: return@addActionListener)), this)
290 | }
291 | btnPaste.addActionListener {
292 | val s = Toolkit.getDefaultToolkit().systemClipboard.getData(DataFlavor.stringFlavor) as? String ?: return@addActionListener
293 | val ls = Load(LoadSettingsBuilder().build())
294 | try {
295 | value = parseMap(ls.loadFromString(s) as Map)
296 | update()
297 | } catch (e: Exception) {
298 | JOptionPane.showMessageDialog(w, e.message)
299 | }
300 | }
301 | }
302 |
303 | override fun lostOwnership(p0: Clipboard?, p1: Transferable?) {} /* ClipboardOwner */
304 | }
305 |
306 | interface ChangeListener {
307 | fun valueChanged(value: E?)
308 | }
309 |
310 | class CollapsedMessageMatchWidget(w: Window, mm: Piper.MessageMatch?, val showHeaderMatch: Boolean, caption: String) :
311 | CollapsedWidget(w, mm, caption, removable = true) {
312 |
313 | override fun editDialog(value: Piper.MessageMatch, parent: Component): Piper.MessageMatch? =
314 | MessageMatchDialog(value, showHeaderMatch = showHeaderMatch, parent = parent).showGUI()
315 |
316 | override fun toHumanReadable(): String =
317 | value?.toHumanReadable(negation = false, hideParentheses = true) ?: "(no filter)"
318 |
319 | override val asMap: Map?
320 | get() = value?.toMap()
321 |
322 | override fun parseMap(map: Map): Piper.MessageMatch = messageMatchFromMap(map)
323 |
324 | override val default: Piper.MessageMatch
325 | get() = Piper.MessageMatch.getDefaultInstance()
326 | }
327 |
328 | class CollapsedCommandInvocationWidget(w: Window, cmd: Piper.CommandInvocation, private val purpose: CommandInvocationPurpose, private val showPassHeaders: Boolean = true) :
329 | CollapsedWidget(w, cmd, "Command: ", removable = (purpose == CommandInvocationPurpose.MATCH_FILTER)) {
330 |
331 | override fun toHumanReadable(): String = (if (purpose == CommandInvocationPurpose.MATCH_FILTER) value?.toHumanReadable(negation = false) else value?.commandLine) ?: "(no command)"
332 | override fun editDialog(value: Piper.CommandInvocation, parent: Component): Piper.CommandInvocation? =
333 | CommandInvocationDialog(value, purpose = purpose, parent = parent, showPassHeaders = showPassHeaders).showGUI()
334 |
335 | override val asMap: Map?
336 | get() = value?.toMap()
337 |
338 | override fun parseMap(map: Map): Piper.CommandInvocation = commandInvocationFromMap(map)
339 |
340 | override val default: Piper.CommandInvocation
341 | get() = Piper.CommandInvocation.getDefaultInstance()
342 | }
343 |
344 | abstract class ConfigDialog(private val parent: Component?, private val caption: String) : JDialog() {
345 | protected val panel = JPanel(GridBagLayout())
346 | protected val cs = GridBagConstraints().apply {
347 | fill = GridBagConstraints.HORIZONTAL
348 | gridx = 0
349 | gridy = 0
350 | }
351 | private var state: E? = null
352 |
353 | fun showGUI(): E? {
354 | addFullWidthComponent(createOkCancelButtonsPanel(), panel, cs)
355 | title = caption
356 | defaultCloseOperation = JFrame.DISPOSE_ON_CLOSE
357 | add(panel)
358 | rootPane.border = BorderFactory.createEmptyBorder(10, 10, 10, 10)
359 | pack()
360 | setLocationRelativeTo(parent)
361 | isModal = true
362 | isVisible = true
363 | return state
364 | }
365 |
366 | private fun createOkCancelButtonsPanel(): Component {
367 | val btnOK = JButton("OK")
368 | val btnCancel = JButton("Cancel")
369 | rootPane.defaultButton = btnOK
370 |
371 | btnOK.addActionListener {
372 | try {
373 | state = processGUI()
374 | isVisible = false
375 | } catch (e: CancelClosingWindow) {
376 | /* do nothing, just skip closing the window */
377 | } catch (e: Exception) {
378 | JOptionPane.showMessageDialog(this, e.message)
379 | }
380 | }
381 |
382 | btnCancel.addActionListener {
383 | isVisible = false
384 | }
385 |
386 | return JPanel().apply {
387 | add(btnOK)
388 | add(btnCancel)
389 | }
390 | }
391 |
392 | abstract fun processGUI(): E
393 | }
394 |
395 | abstract class MinimalToolDialog(private val common: Piper.MinimalTool, parent: Component?, noun: String,
396 | showPassHeaders: Boolean = true, showScope: Boolean = false,
397 | showFilter: Boolean = true,
398 | purpose: CommandInvocationPurpose = CommandInvocationPurpose.SELF_FILTER) :
399 | ConfigDialog(parent, if (common.name.isEmpty()) "Add $noun" else "Edit $noun \"${common.name}\"") {
400 | private val mtw = MinimalToolWidget(common, panel, cs, this, showPassHeaders = showPassHeaders,
401 | purpose = purpose, showScope = showScope, showFilter = showFilter)
402 |
403 | override fun processGUI(): E = processGUI(mtw.toMinimalTool())
404 |
405 | fun isToolEnabled() : Boolean = common.enabled
406 | fun toHumanReadable(): String = minimalToolHumanReadableName(common)
407 |
408 | abstract fun buildEnabled(value: Boolean) : E
409 | abstract fun processGUI(mt: Piper.MinimalTool): E
410 |
411 | protected fun addFilterChangeListener(listener: ChangeListener) {
412 | mtw.addFilterChangeListener(listener)
413 | }
414 | }
415 |
416 | class MessageViewerDialog(private val messageViewer: Piper.MessageViewer, parent: Component?) :
417 | MinimalToolDialog(messageViewer.common, parent, "message viewer", showScope = true) {
418 |
419 | private val cbUsesColors = createFullWidthCheckBox("Uses ANSI (color) escape sequences", messageViewer.usesColors, panel, cs)
420 |
421 | override fun processGUI(mt: Piper.MinimalTool): Piper.MessageViewer = Piper.MessageViewer.newBuilder().apply {
422 | common = mt
423 | if (cbUsesColors.isSelected) usesColors = true
424 | }.build()
425 |
426 | override fun buildEnabled(value: Boolean): Piper.MessageViewer = messageViewer.buildEnabled(value)
427 | }
428 |
429 | const val HTTP_LISTENER_NOTE = "Note: Piper settings are global and thus apply to all your Burp projects. HTTP listeners without filters might have hard-to-debug side effects, you've been warned."
430 |
431 | class HttpListenerDialog(private val httpListener: Piper.HttpListener, parent: Component?) :
432 | MinimalToolDialog(httpListener.common, parent, "HTTP listener") {
433 |
434 | private val lsScope = createLabeledWidget("Listen to ",
435 | JComboBox(ConfigHttpListenerScope.values()).apply { selectedItem = ConfigHttpListenerScope.fromHttpListenerScope(httpListener.scope) }, panel, cs)
436 | private val btw = EnumSetWidget(httpListener.toolSet, panel, cs, "sent/received by", BurpTool::class.java)
437 | private val cbIgnore = createFullWidthCheckBox("Ignore output (if you only need side effects)", httpListener.ignoreOutput, panel, cs)
438 | private val lbNote = addFullWidthComponent(JLabel(HTTP_LISTENER_NOTE), panel, cs)
439 |
440 | init {
441 | addFilterChangeListener(object : ChangeListener {
442 | override fun valueChanged(value: Piper.MessageMatch?) {
443 | lbNote.isVisible = value == null
444 | repack()
445 | }
446 | })
447 | }
448 |
449 | override fun processGUI(mt: Piper.MinimalTool): Piper.HttpListener {
450 | val bt = btw.toSet()
451 | return Piper.HttpListener.newBuilder().apply {
452 | common = mt
453 | scope = (lsScope.selectedItem as ConfigHttpListenerScope).hls
454 | if (cbIgnore.isSelected) ignoreOutput = true
455 | if (bt.size < BurpTool.values().size) setToolSet(bt)
456 | }.build()
457 | }
458 |
459 | override fun buildEnabled(value: Boolean): Piper.HttpListener = httpListener.buildEnabled(value)
460 | }
461 |
462 | class CommentatorDialog(private val commentator: Piper.Commentator, parent: Component?) :
463 | MinimalToolDialog(commentator.common, parent, "commentator", showScope = true) {
464 |
465 | private val cbOverwrite: JCheckBox = createFullWidthCheckBox("Overwrite comments on items that already have one", commentator.overwrite, panel, cs)
466 | private val cbListener: JCheckBox = createFullWidthCheckBox("Continuously apply to future requests/responses", commentator.applyWithListener, panel, cs)
467 |
468 | override fun processGUI(mt: Piper.MinimalTool): Piper.Commentator = Piper.Commentator.newBuilder().apply {
469 | common = mt
470 | if (cbOverwrite.isSelected) overwrite = true
471 | if (cbListener.isSelected) applyWithListener = true
472 | }.build()
473 |
474 | override fun buildEnabled(value: Boolean): Piper.Commentator = commentator.buildEnabled(value)
475 | }
476 |
477 | class HighlighterDialog(private val highlighter: Piper.Highlighter, parent: Component?) :
478 | MinimalToolDialog(highlighter.common, parent, "highlighter", showScope = true) {
479 |
480 | private val cbOverwrite: JCheckBox = createFullWidthCheckBox("Overwrite highlight on items that already have one", highlighter.overwrite, panel, cs)
481 | private val cbListener: JCheckBox = createFullWidthCheckBox("Continuously apply to future requests/responses", highlighter.applyWithListener, panel, cs)
482 | private val cbColor = createLabeledWidget("Set highlight to ", JComboBox(Highlight.values()), panel, cs)
483 |
484 | init {
485 | cbColor.renderer = object : DefaultListCellRenderer() {
486 | override fun getListCellRendererComponent(list: JList<*>?, value: Any?, index: Int, isSelected: Boolean, cellHasFocus: Boolean): Component {
487 | val c = super.getListCellRendererComponent(list, value, index, isSelected, cellHasFocus)
488 | val v = value as Highlight
489 | if (v.color != null) {
490 | c.background = v.color
491 | c.foreground = v.textColor
492 | }
493 | return c
494 | }
495 | }
496 | val h = Highlight.fromString(highlighter.color)
497 | if (h != null) cbColor.selectedItem = h
498 | }
499 |
500 | override fun processGUI(mt: Piper.MinimalTool): Piper.Highlighter = Piper.Highlighter.newBuilder().apply {
501 | common = mt
502 | color = cbColor.selectedItem.toString()
503 | if (cbOverwrite.isSelected) overwrite = true
504 | if (cbListener.isSelected) applyWithListener = true
505 | }.build()
506 |
507 | override fun buildEnabled(value: Boolean): Piper.Highlighter = highlighter.buildEnabled(value)
508 | }
509 |
510 | private fun createFullWidthCheckBox(caption: String, initialValue: Boolean, panel: Container, cs: GridBagConstraints): JCheckBox {
511 | cs.gridwidth = 4
512 | cs.gridx = 0
513 | cs.gridy++
514 | return createCheckBox(caption, initialValue, panel, cs)
515 | }
516 |
517 | private fun createCheckBox(caption: String, initialValue: Boolean, panel: Container, cs: GridBagConstraints): JCheckBox {
518 | val cb = JCheckBox(caption)
519 | cb.isSelected = initialValue
520 | panel.add(cb, cs)
521 | return cb
522 | }
523 |
524 | class MenuItemDialog(private val menuItem: Piper.UserActionTool, parent: Component?) :
525 | MinimalToolDialog(menuItem.common, parent, "menu item",
526 | purpose = CommandInvocationPurpose.EXECUTE_ONLY, showScope = true) {
527 |
528 | private val cbHasGUI: JCheckBox = createFullWidthCheckBox("Has its own GUI (no need for a console window)", menuItem.hasGUI, panel, cs)
529 | private val cbAvoidPipe: JCheckBox = createFullWidthCheckBox("Avoid piping into this tool (reduces clutter in menu if it doesn't make sense)", menuItem.avoidPipe, panel, cs)
530 | private val smMinInputs: SpinnerNumberModel = createSpinner("Minimum required number of selected items: ",
531 | max(menuItem.minInputs, 1), 1, panel, cs)
532 | private val smMaxInputs: SpinnerNumberModel = createSpinner("Maximum allowed number of selected items: (0 = no limit) ",
533 | menuItem.maxInputs, 0, panel, cs)
534 |
535 | override fun processGUI(mt: Piper.MinimalTool): Piper.UserActionTool {
536 | val minInputsValue = smMinInputs.number.toInt()
537 | val maxInputsValue = smMaxInputs.number.toInt()
538 |
539 | if (maxInputsValue in 1 until minInputsValue) throw RuntimeException(
540 | "Maximum allowed number of selected items cannot be lower than minimum required number of selected items.")
541 |
542 | return Piper.UserActionTool.newBuilder().apply {
543 | common = mt
544 | if (cbHasGUI.isSelected) hasGUI = true
545 | if (cbAvoidPipe.isSelected) avoidPipe = true
546 | if (minInputsValue > 1) minInputs = minInputsValue
547 | if (maxInputsValue > 0) maxInputs = maxInputsValue
548 | }.build()
549 | }
550 |
551 | override fun buildEnabled(value: Boolean): Piper.UserActionTool = menuItem.buildEnabled(value)
552 | }
553 |
554 | private fun createSpinner(caption: String, initial: Int, minimum: Int, panel: Container, cs: GridBagConstraints): SpinnerNumberModel {
555 | val model = SpinnerNumberModel(initial, minimum, Integer.MAX_VALUE, 1)
556 |
557 | cs.gridy++
558 | cs.gridwidth = 2
559 | cs.gridx = 0 ; panel.add(JLabel(caption), cs)
560 | cs.gridx = 2 ; panel.add(JSpinner(model), cs)
561 |
562 | return model
563 | }
564 |
565 | class IntruderPayloadProcessorDialog(private val ipp: Piper.MinimalTool, parent: Component?) :
566 | MinimalToolDialog(ipp, parent, "Intruder payload processor", showPassHeaders = false) {
567 |
568 | override fun processGUI(mt: Piper.MinimalTool): Piper.MinimalTool = mt
569 | override fun buildEnabled(value: Boolean): Piper.MinimalTool = ipp.buildEnabled(value)
570 | }
571 |
572 | class IntruderPayloadGeneratorDialog(private val ipp: Piper.MinimalTool, parent: Component?) :
573 | MinimalToolDialog(ipp, parent, "Intruder payload generator",
574 | showPassHeaders = false, showFilter = false) {
575 |
576 | override fun processGUI(mt: Piper.MinimalTool): Piper.MinimalTool = mt
577 | override fun buildEnabled(value: Boolean): Piper.MinimalTool = ipp.buildEnabled(value)
578 | }
579 |
580 | class MacroDialog(private val macro: Piper.MinimalTool, parent: Component?) :
581 | MinimalToolDialog(macro, parent, "macro") {
582 |
583 | override fun processGUI(mt: Piper.MinimalTool): Piper.MinimalTool = mt
584 | override fun buildEnabled(value: Boolean): Piper.MinimalTool = macro.buildEnabled(value)
585 | }
586 |
587 | fun createLabeledTextField(caption: String, initialValue: String, panel: Container, cs: GridBagConstraints): JTextField {
588 | return createLabeledWidget(caption, JTextField(initialValue), panel, cs)
589 | }
590 |
591 | fun createLabeledComboBox(caption: String, initialValue: String, panel: Container, cs: GridBagConstraints, choices: Array): JComboBox {
592 | val cb = JComboBox(choices)
593 | cb.isEditable = true
594 | cb.selectedItem = initialValue
595 | return createLabeledWidget(caption, cb, panel, cs)
596 | }
597 |
598 | fun createLabeledWidget(caption: String, widget: T, panel: Container, cs: GridBagConstraints): T {
599 | cs.gridy++
600 | cs.gridwidth = 1 ; cs.gridx = 0 ; panel.add(JLabel(caption), cs)
601 | cs.gridwidth = 3 ; cs.gridx = 1 ; panel.add(widget, cs)
602 | return widget
603 | }
604 |
605 | class HeaderMatchDialog(hm: Piper.HeaderMatch, parent: Component) : ConfigDialog(parent, "Header filter editor") {
606 | private val commonHeaders = arrayOf("Content-Disposition", "Content-Type", "Cookie",
607 | "Host", "Origin", "Referer", "Server", "User-Agent", "X-Requested-With")
608 | private val cbHeader = createLabeledComboBox("Header name: (case insensitive) ", hm.header, panel, cs, commonHeaders)
609 | private val regExpWidget: RegExpWidget = RegExpWidget(hm.regex, panel, cs)
610 |
611 | override fun processGUI(): Piper.HeaderMatch {
612 | val text = cbHeader.selectedItem?.toString()
613 | if (text.isNullOrEmpty()) throw RuntimeException("The header name cannot be empty.")
614 |
615 | return Piper.HeaderMatch.newBuilder().apply {
616 | header = text
617 | regex = regExpWidget.toRegularExpression()
618 | }.build()
619 | }
620 | }
621 |
622 | const val CMDLINE_INPUT_FILENAME_PLACEHOLDER = ""
623 | const val CMDLINE_EMPTY_STRING_PLACEHOLDER = ""
624 |
625 | data class CommandLineParameter(val value: String?) { // null = input file name
626 | val isInputFileName: Boolean
627 | get() = value == null
628 | val isEmptyString: Boolean
629 | get() = value?.isEmpty() == true
630 | override fun toString(): String = when {
631 | isInputFileName -> CMDLINE_INPUT_FILENAME_PLACEHOLDER
632 | value.isNullOrEmpty() -> CMDLINE_EMPTY_STRING_PLACEHOLDER // empty strings would be rendered as a barely visible 1 to 2 px high item
633 | else -> value
634 | }
635 | }
636 |
637 | const val PASS_HTTP_HEADERS_NOTE = "Note: if the above checkbox is unchecked, messages without a body (such as " +
638 | "GET/HEAD requests or 204 No Content responses) are ignored by this tool."
639 |
640 | class CommandInvocationDialog(ci: Piper.CommandInvocation, private val purpose: CommandInvocationPurpose, parent: Component,
641 | showPassHeaders: Boolean) : ConfigDialog(parent, "Command invocation editor") {
642 | private val ccmwStdout = CollapsedMessageMatchWidget(this, mm = ci.stdout, showHeaderMatch = false, caption = "Match on stdout: ")
643 | private val ccmwStderr = CollapsedMessageMatchWidget(this, mm = ci.stderr, showHeaderMatch = false, caption = "Match on stderr: ")
644 | private val monospaced12 = Font("monospaced", Font.PLAIN, 12)
645 | private var tfExitCode: JTextField? = null
646 | private val cbPassHeaders: JCheckBox?
647 | private val tfDependencies = JTextField()
648 |
649 | private val hasFileName = ci.inputMethod == Piper.CommandInvocation.InputMethod.FILENAME
650 | private val paramsModel = fillDefaultModel(sequence {
651 | yieldAll(ci.prefixList)
652 | if (hasFileName) yield(null)
653 | yieldAll(ci.postfixList)
654 | }.map(::CommandLineParameter))
655 |
656 | fun parseExitCodeList(): Iterable {
657 | val text = tfExitCode!!.text
658 | return if (text.isEmpty()) emptyList()
659 | else text.filterNot(Char::isWhitespace).split(',').map(String::toInt)
660 | }
661 |
662 | init {
663 | val lsParams = JList(paramsModel)
664 | lsParams.selectionMode = ListSelectionModel.MULTIPLE_INTERVAL_SELECTION
665 | lsParams.font = monospaced12
666 |
667 | lsParams.cellRenderer = object : DefaultListCellRenderer() {
668 | override fun getListCellRendererComponent(list: JList<*>?, value: Any?, index: Int, isSelected: Boolean, cellHasFocus: Boolean): Component {
669 | val c = super.getListCellRendererComponent(list, value, index, isSelected, cellHasFocus)
670 | val v = value as CommandLineParameter
671 | if (v.isInputFileName) {
672 | c.background = Color.RED
673 | c.foreground = if (isSelected) Color.YELLOW else Color.WHITE
674 | } else if (v.isEmptyString) {
675 | c.background = Color.YELLOW
676 | c.foreground = if (isSelected) Color.RED else Color.BLUE
677 | }
678 | return c
679 | }
680 | }
681 |
682 | lsParams.addDoubleClickListener {
683 | if (paramsModel[it].isInputFileName) {
684 | JOptionPane.showMessageDialog(this, CMDLINE_INPUT_FILENAME_PLACEHOLDER +
685 | " is a special placeholder for the names of the input file(s), and thus cannot be edited.")
686 | return@addDoubleClickListener
687 | }
688 | paramsModel[it] = CommandLineParameter(
689 | JOptionPane.showInputDialog(this, "Edit command line parameter no. ${it + 1}:", paramsModel[it].value)
690 | ?: return@addDoubleClickListener)
691 | }
692 |
693 | cs.gridwidth = 4
694 |
695 | panel.add(JLabel("Command line parameters: (one per line)"), cs)
696 |
697 | cs.gridy = 1
698 | cs.gridwidth = 3
699 | cs.gridheight = 3
700 | cs.fill = GridBagConstraints.BOTH
701 |
702 | panel.add(JScrollPane(lsParams), cs)
703 |
704 | val btnMoveUp = JButton("Move up")
705 | btnMoveUp.addActionListener {
706 | val si = lsParams.selectedIndices
707 | if (si.isEmpty() || si[0] == 0) return@addActionListener
708 | si.forEach {
709 | paramsModel.insertElementAt(paramsModel.remove(it - 1), it)
710 | }
711 | }
712 |
713 | val btnMoveDown = JButton("Move down")
714 | btnMoveDown.addActionListener {
715 | val si = lsParams.selectedIndices
716 | if (si.isEmpty() || si.last() == paramsModel.size - 1) return@addActionListener
717 | si.reversed().forEach {
718 | paramsModel.insertElementAt(paramsModel.remove(it + 1), it)
719 | }
720 | lsParams.selectedIndices = si.map { it + 1 }.toIntArray()
721 | }
722 |
723 | cs.gridx = 3
724 | cs.gridwidth = 1
725 | cs.gridheight = 1
726 | cs.fill = GridBagConstraints.HORIZONTAL
727 |
728 | panel.add(createRemoveButton(lsParams, paramsModel), cs)
729 |
730 | cs.gridy = 2; panel.add(btnMoveUp, cs)
731 | cs.gridy = 3; panel.add(btnMoveDown, cs)
732 |
733 | val tfParam = JTextField()
734 | val btnAdd = JButton("Add")
735 |
736 | btnAdd.addActionListener {
737 | paramsModel.addElement(CommandLineParameter(tfParam.text))
738 | tfParam.text = ""
739 | }
740 |
741 | cs.gridy = 4
742 |
743 | cs.gridx = 0; cs.gridwidth = 1; panel.add(JLabel("Add parameter: "), cs)
744 | cs.gridx = 1; cs.gridwidth = 2; panel.add(tfParam, cs)
745 | cs.gridx = 3; cs.gridwidth = 1; panel.add(btnAdd, cs)
746 |
747 | val cbSpace = createFullWidthCheckBox("Auto-add upon pressing space or closing quotes", true, panel, cs)
748 |
749 | tfParam.font = monospaced12
750 | tfParam.addKeyListener(object : KeyAdapter() {
751 | override fun keyTyped(e: KeyEvent) {
752 | if (cbSpace.isSelected) {
753 | val t = tfParam.text
754 | if (t.startsWith('"')) {
755 | if (e.keyChar == '"') {
756 | tfParam.text = t.substring(1)
757 | btnAdd.doClick()
758 | e.consume()
759 | }
760 | } else if (t.startsWith('\'')) {
761 | if (e.keyChar == '\'') {
762 | tfParam.text = t.substring(1)
763 | btnAdd.doClick()
764 | e.consume()
765 | }
766 | } else if (e.keyChar == ' ' && t.isNotEmpty()) {
767 | btnAdd.doClick()
768 | e.consume()
769 | }
770 | }
771 | }
772 | })
773 |
774 | cs.gridy = 6
775 |
776 | InputMethodWidget.create(this, panel, cs, hasFileName, paramsModel)
777 |
778 | cbPassHeaders = if (showPassHeaders) {
779 | val cb = createFullWidthCheckBox("Pass HTTP headers to command", ci.passHeaders, panel, cs)
780 | addFullWidthComponent(JLabel(PASS_HTTP_HEADERS_NOTE), panel, cs)
781 | cb
782 | } else null
783 |
784 | addFullWidthComponent(JLabel("Binaries required in PATH: (comma separated)"), panel, cs)
785 | addFullWidthComponent(tfDependencies, panel, cs)
786 | tfDependencies.text = ci.requiredInPathList.joinToString(separator = ", ")
787 |
788 | if (purpose != CommandInvocationPurpose.EXECUTE_ONLY) {
789 | val exitValues = ci.exitCodeList.joinToString(", ")
790 |
791 | if (purpose == CommandInvocationPurpose.SELF_FILTER) {
792 | addFullWidthComponent(JLabel("If any filters are set below, they are treated the same way as a pre-exec filter."), panel, cs)
793 | }
794 | ccmwStdout.buildGUI(panel, cs)
795 | ccmwStderr.buildGUI(panel, cs)
796 | val tfExitCode = createLabeledTextField("Match on exit code: (comma separated) ", exitValues, panel, cs)
797 |
798 | tfExitCode.inputVerifier = object : InputVerifier() {
799 | override fun verify(input: JComponent?): Boolean =
800 | try {
801 | parseExitCodeList(); true
802 | } catch (e: NumberFormatException) {
803 | false
804 | }
805 | }
806 |
807 | this.tfExitCode = tfExitCode
808 | }
809 | }
810 |
811 | override fun processGUI(): Piper.CommandInvocation = Piper.CommandInvocation.newBuilder().apply {
812 | if (purpose != CommandInvocationPurpose.EXECUTE_ONLY) {
813 | if (ccmwStdout.value != null) stdout = ccmwStdout.value
814 | if (ccmwStderr.value != null) stderr = ccmwStderr.value
815 | try {
816 | addAllExitCode(parseExitCodeList())
817 | } catch (e: NumberFormatException) {
818 | throw RuntimeException("Exit codes should contain numbers separated by commas only. (Whitespace is ignored.)")
819 | }
820 | if (purpose == CommandInvocationPurpose.MATCH_FILTER && !hasFilter) {
821 | throw RuntimeException("No filters are defined for stdio or exit code.")
822 | }
823 | }
824 | val d = tfDependencies.text.replace("\\s".toRegex(), "")
825 | if (d.isNotEmpty()) addAllRequiredInPath(d.split(','))
826 | if (paramsModel.isEmpty) throw RuntimeException("The command must contain at least one argument.")
827 | if (paramsModel[0].isEmptyString) throw RuntimeException("The first argument (the command) is an empty string")
828 | val params = paramsModel.map(CommandLineParameter::value)
829 | addAllPrefix(params.takeWhile(Objects::nonNull))
830 | if (prefixCount < paramsModel.size) {
831 | inputMethod = Piper.CommandInvocation.InputMethod.FILENAME
832 | addAllPostfix(params.drop(prefixCount + 1))
833 | }
834 | if (cbPassHeaders?.isSelected == true) passHeaders = true
835 | }.build()
836 | }
837 |
838 | private class InputMethodWidget(private val w: Window, private val label: JLabel = JLabel(),
839 | private val button: JButton = JButton(),
840 | private var hasFileName: Boolean) {
841 | fun update() {
842 | label.text = "Input method: " + (if (hasFileName) "filename" else "standard input") + " "
843 | button.text = if (hasFileName) "Set to stdin (remove $CMDLINE_INPUT_FILENAME_PLACEHOLDER)"
844 | else "Set to filename (add $CMDLINE_INPUT_FILENAME_PLACEHOLDER)"
845 | w.repack()
846 | }
847 |
848 | companion object {
849 | fun create(w: Window, panel: Container, cs: GridBagConstraints, hasFileName: Boolean, paramsModel: DefaultListModel): InputMethodWidget {
850 | val imw = InputMethodWidget(w = w, hasFileName = hasFileName)
851 | imw.update()
852 | cs.gridwidth = 2
853 | cs.gridx = 0 ; panel.add(imw.label, cs)
854 | cs.gridx = 2 ; panel.add(imw.button, cs)
855 |
856 | paramsModel.addListDataListener(object : ListDataListener {
857 | override fun intervalRemoved(p0: ListDataEvent?) {
858 | if (!imw.hasFileName || paramsModel.toIterable().any(CommandLineParameter::isInputFileName)) return
859 | imw.hasFileName = false
860 | imw.update()
861 | }
862 |
863 | override fun contentsChanged(p0: ListDataEvent?) { /* ignore */ }
864 | override fun intervalAdded(p0: ListDataEvent?) { /* ignore */ }
865 | })
866 |
867 | imw.button.addActionListener {
868 | if (imw.hasFileName) {
869 | val iof = paramsModel.toIterable().indexOfFirst(CommandLineParameter::isInputFileName)
870 | if (iof >= 0) paramsModel.remove(iof) // this triggers intervalRemoved above, no explicit update() necessary
871 | } else {
872 | paramsModel.addElement(CommandLineParameter(null))
873 | imw.hasFileName = true
874 | imw.update()
875 | }
876 | }
877 |
878 | return imw
879 | }
880 | }
881 | }
882 |
883 | private fun addFullWidthComponent(c: E, panel: Container, cs: GridBagConstraints): E {
884 | cs.gridx = 0
885 | cs.gridy++
886 | cs.gridwidth = 4
887 |
888 | panel.add(c, cs)
889 | return c
890 | }
891 |
892 | class HexASCIITextField(private val tf: JTextField = JTextField(),
893 | private val rbHex: JRadioButton = JRadioButton("Hex"),
894 | private val rbASCII: JRadioButton = JRadioButton("ASCII"),
895 | private val field: String, private var isASCII: Boolean) {
896 |
897 | constructor(field: String, source: ByteString, dialog: Component) : this(field=field, isASCII=source.isValidUtf8) {
898 | if (isASCII) {
899 | tf.text = source.toStringUtf8()
900 | rbASCII.isSelected = true
901 | } else {
902 | tf.text = source.toHexPairs()
903 | rbHex.isSelected = true
904 | }
905 |
906 | with(ButtonGroup()) { add(rbHex); add(rbASCII); }
907 |
908 | rbASCII.addActionListener {
909 | if (isASCII) return@addActionListener
910 | val bytes = try { parseHex() } catch(e: NumberFormatException) {
911 | JOptionPane.showMessageDialog(dialog, "Error in $field field: hexadecimal string ${e.message}")
912 | rbHex.isSelected = true
913 | return@addActionListener
914 | }
915 | tf.text = String(bytes, Charsets.UTF_8)
916 | isASCII = true
917 | }
918 |
919 | rbHex.addActionListener {
920 | if (!isASCII) return@addActionListener
921 | tf.text = tf.text.toByteArray(/* default is UTF-8 */).toHexPairs()
922 | isASCII = false
923 | }
924 | }
925 |
926 | private fun parseHex(): ByteArray = tf.text.filter(Char::isLetterOrDigit).run {
927 | if (length % 2 != 0) {
928 | throw NumberFormatException("needs to contain an even number of hex digits")
929 | }
930 | if (any { c -> c in 'g'..'z' || c in 'G'..'Z' }) {
931 | throw NumberFormatException("contains non-hexadecimal letters (maybe typo?)")
932 | }
933 | chunked(2, ::parseHexByte).toByteArray()
934 | }
935 |
936 | fun getByteString(): ByteString = if (isASCII) ByteString.copyFromUtf8(tf.text) else try {
937 | ByteString.copyFrom(parseHex())
938 | } catch (e: NumberFormatException) {
939 | throw RuntimeException("Error in $field field: hexadecimal string ${e.message}")
940 | }
941 |
942 | fun addWidgets(caption: String, cs: GridBagConstraints, panel: Container) {
943 | cs.gridy++
944 | cs.gridx = 0 ; panel.add(JLabel(caption), cs)
945 | cs.gridx = 1 ; panel.add(tf, cs)
946 | cs.gridx = 2 ; panel.add(rbASCII, cs)
947 | cs.gridx = 3 ; panel.add(rbHex, cs)
948 | }
949 | }
950 |
951 | private fun parseHexByte(cs: CharSequence): Byte = (parseHexNibble(cs[0]) shl 4 or parseHexNibble(cs[1])).toByte()
952 |
953 | private fun parseHexNibble(c: Char): Int = if (c in '0'..'9') (c - '0')
954 | else ((c.toLowerCase() - 'a') + 0xA)
955 |
956 | class RegExpWidget(regex: Piper.RegularExpression, panel: Container, cs: GridBagConstraints) {
957 | private val tfPattern = createLabeledTextField("Matches regular expression: ", regex.pattern, panel, cs)
958 | private val esw = EnumSetWidget(regex.flagSet, panel, cs, "Regular expression flags: (see JDK documentation)", RegExpFlag::class.java)
959 |
960 | fun hasPattern(): Boolean = tfPattern.text.isNotEmpty()
961 |
962 | fun toRegularExpression(): Piper.RegularExpression {
963 | return Piper.RegularExpression.newBuilder().setPattern(tfPattern.text).setFlagSet(esw.toSet()).build().apply { compile() }
964 | }
965 | }
966 |
967 | class EnumSetWidget>(set: Set, panel: Container, cs: GridBagConstraints, caption: String, enumClass: Class) {
968 | private val cbMap: Map
969 |
970 | fun toSet(): Set = cbMap.filterValues(JCheckBox::isSelected).keys
971 |
972 | init {
973 | addFullWidthComponent(JLabel(caption), panel, cs)
974 | cs.gridy++
975 | cs.gridwidth = 1
976 |
977 | cbMap = enumClass.enumConstants.asIterable().associateWithTo(EnumMap(enumClass)) {
978 | val cb = createCheckBox(it.toString(), it in set, panel, cs)
979 | if (cs.gridx == 0) {
980 | cs.gridx = 1
981 | } else {
982 | cs.gridy++
983 | cs.gridx = 0
984 | }
985 | cb
986 | }.toMap()
987 | }
988 | }
989 |
990 | class CollapsedHeaderMatchWidget(w: Window, hm: Piper.HeaderMatch?) :
991 | CollapsedWidget(w, hm, "Header: ", removable = true) {
992 |
993 | override fun editDialog(value: Piper.HeaderMatch, parent: Component): Piper.HeaderMatch? =
994 | HeaderMatchDialog(value, parent = parent).showGUI()
995 |
996 | override fun toHumanReadable(): String = value?.toHumanReadable(negation = false) ?: "(no header match)"
997 |
998 | override val asMap: Map?
999 | get() = value?.toMap()
1000 |
1001 | override fun parseMap(map: Map): Piper.HeaderMatch = HeaderMatchFromMap.invoke(map)
1002 |
1003 | override val default: Piper.HeaderMatch
1004 | get() = Piper.HeaderMatch.getDefaultInstance()
1005 | }
1006 |
1007 | class MessageMatchDialog(mm: Piper.MessageMatch, private val showHeaderMatch: Boolean, parent: Component) : ConfigDialog(parent, "Filter editor") {
1008 | private val prefixField = HexASCIITextField("prefix", mm.prefix, this)
1009 | private val postfixField = HexASCIITextField("postfix", mm.postfix, this)
1010 | private val cciw = CollapsedCommandInvocationWidget(this, mm.cmd, CommandInvocationPurpose.MATCH_FILTER)
1011 | private val chmw = CollapsedHeaderMatchWidget(this, mm.header)
1012 | private val cbNegation = JComboBox(MatchNegation.values())
1013 | private val regExpWidget: RegExpWidget
1014 | private val cbInScope: JCheckBox?
1015 | private val andAlsoPanel = MatchListEditor("All of these apply: [AND]", mm.andAlsoList)
1016 | private val orElsePanel = MatchListEditor("Any of these apply: [OR]", mm.orElseList)
1017 |
1018 | init {
1019 | cs.gridwidth = 4
1020 |
1021 | panel.add(cbNegation, cs)
1022 | cbNegation.selectedItem = if (mm.negation) MatchNegation.NEGATED else MatchNegation.NORMAL
1023 |
1024 | cs.gridwidth = 1
1025 |
1026 | prefixField .addWidgets("Starts with: ", cs, panel)
1027 | postfixField.addWidgets( "Ends with: ", cs, panel)
1028 | regExpWidget = RegExpWidget(mm.regex, panel, cs)
1029 |
1030 | if (showHeaderMatch) chmw.buildGUI(panel, cs)
1031 |
1032 | cciw.buildGUI(panel, cs)
1033 |
1034 | cbInScope = if (showHeaderMatch) createFullWidthCheckBox("request is in Burp Suite scope", mm.inScope, panel, cs) else null
1035 |
1036 | val spList = JSplitPane()
1037 | spList.leftComponent = andAlsoPanel
1038 | spList.rightComponent = orElsePanel
1039 |
1040 | addFullWidthComponent(spList, panel, cs)
1041 |
1042 | cs.gridy++
1043 | }
1044 |
1045 | override fun processGUI(): Piper.MessageMatch {
1046 | val builder = Piper.MessageMatch.newBuilder()
1047 |
1048 | if ((cbNegation.selectedItem as MatchNegation).negation) builder.negation = true
1049 |
1050 | builder.postfix = postfixField.getByteString()
1051 | builder.prefix = prefixField.getByteString()
1052 |
1053 | if (regExpWidget.hasPattern()) builder.regex = regExpWidget.toRegularExpression()
1054 |
1055 | if (chmw.value != null) builder.header = chmw.value
1056 |
1057 | if (cbInScope != null && cbInScope.isSelected) builder.inScope = true
1058 |
1059 | val cmd = cciw.value
1060 | if (cmd != null) {
1061 | try {
1062 | cmd.checkDependencies()
1063 | } catch (c: DependencyException) {
1064 | if (JOptionPane.showConfirmDialog(panel, "${c.message}\n\nAre you sure you want to save this?",
1065 | "Confirmation", JOptionPane.YES_NO_OPTION) != JOptionPane.YES_OPTION) throw CancelClosingWindow()
1066 | }
1067 | builder.cmd = cmd
1068 | }
1069 |
1070 | builder.addAllAndAlso(andAlsoPanel.items)
1071 | builder.addAllOrElse ( orElsePanel.items)
1072 |
1073 | return builder.build()
1074 | }
1075 |
1076 | inner class MatchListEditor(caption: String, source: List) : ClipboardOwner,
1077 | ListEditor(fillDefaultModel(source), this, caption) {
1078 | override fun addDialog(): Piper.MessageMatch? = MessageMatchDialog(Piper.MessageMatch.getDefaultInstance(),
1079 | showHeaderMatch = showHeaderMatch, parent = this).showGUI()
1080 |
1081 | override fun editDialog(value: Piper.MessageMatch): Piper.MessageMatch? =
1082 | MessageMatchDialog(value, showHeaderMatch = showHeaderMatch, parent = this).showGUI()
1083 |
1084 | override fun toHumanReadable(value: Piper.MessageMatch): String =
1085 | value.toHumanReadable(negation = false, hideParentheses = true)
1086 |
1087 | val items: Iterable
1088 | get() = model.toIterable()
1089 |
1090 | private val btnCopy = JButton("Copy")
1091 | private val btnPaste = JButton("Paste")
1092 |
1093 | override fun updateBtnEnableDisableState() {
1094 | super.updateBtnEnableDisableState()
1095 | updateEnableDisableBtnState()
1096 | }
1097 |
1098 | private fun updateEnableDisableBtnState() {
1099 | btnCopy.isEnabled = listWidget.selectedIndices.isNotEmpty()
1100 | }
1101 |
1102 | init {
1103 | btnCopy.addActionListener {
1104 | Toolkit.getDefaultToolkit().systemClipboard.setContents(StringSelection(
1105 | Dump(DumpSettingsBuilder().build()).dumpToString(listWidget.selectedValue.toMap())), this)
1106 | }
1107 | btnPaste.addActionListener {
1108 | val s = Toolkit.getDefaultToolkit().systemClipboard.getData(DataFlavor.stringFlavor) as? String ?: return@addActionListener
1109 | val ls = Load(LoadSettingsBuilder().build())
1110 | try {
1111 | model.addElement(messageMatchFromMap(ls.loadFromString(s) as Map))
1112 | } catch (e: Exception) {
1113 | JOptionPane.showMessageDialog(this, e.message)
1114 | }
1115 | }
1116 | pnToolbar.add(btnCopy)
1117 | pnToolbar.add(btnPaste)
1118 | updateEnableDisableBtnState()
1119 | }
1120 |
1121 | override fun lostOwnership(p0: Clipboard?, p1: Transferable?) {} /* ClipboardOwner */
1122 | }
1123 | }
1124 |
1125 | fun createRemoveButton(listWidget: JList, listModel: DefaultListModel): JButton {
1126 | val btn = JButton("Remove")
1127 | btn.isEnabled = listWidget.selectedIndices.isNotEmpty()
1128 | listWidget.addListSelectionListener {
1129 | btn.isEnabled = listWidget.selectedIndices.isNotEmpty()
1130 | }
1131 | btn.addActionListener {
1132 | listWidget.selectedIndices.reversed().map(listModel::remove)
1133 | }
1134 | return btn
1135 | }
1136 |
1137 | fun fillDefaultModel(source: Iterable, model: DefaultListModel = DefaultListModel()): DefaultListModel =
1138 | fillDefaultModel(source.asSequence(), model)
1139 | fun fillDefaultModel(source: Sequence, model: DefaultListModel = DefaultListModel()): DefaultListModel {
1140 | model.clear()
1141 | source.forEach(model::addElement)
1142 | return model
1143 | }
1144 |
1145 | fun showModalDialog(width: Int, height: Int, widget: Component, caption: String, dialog: JDialog, parent: Component?) {
1146 | with(dialog) {
1147 | defaultCloseOperation = JFrame.DISPOSE_ON_CLOSE
1148 | add(widget)
1149 | setSize(width, height)
1150 | setLocationRelativeTo(parent)
1151 | title = caption
1152 | isModal = true
1153 | isVisible = true
1154 | }
1155 | }
--------------------------------------------------------------------------------