diff --git a/.gitignore b/.gitignore index c0e8673..eff4362 100644 --- a/.gitignore +++ b/.gitignore @@ -1,65 +1,43 @@ - -.metadata -bin/ -tmp/ -*.tmp -*.bak -*.swp -*~.nib -local.properties -.settings/ -.loadpath -.recommenders - -# IDEA -.idea - - -# Eclipse Core -.project - -# External tool builders -.externalToolBuilders/ - -# Locally stored "Eclipse launch configurations" -*.launch - -# PyDev specific (Python IDE for Eclipse) -*.pydevproject - -# CDT-specific (C/C++ Development Tooling) -.cproject - -# JDT-specific (Eclipse Java Development Tools) +.gradle +build/ +!gradle/wrapper/gradle-wrapper.jar +!**/src/main/**/build/ +!**/src/test/**/build/ + +### IntelliJ IDEA ### +.idea/modules.xml +.idea/jarRepositories.xml +.idea/compiler.xml +.idea/libraries/ +*.iws +*.iml +*.ipr +out/ +!**/src/main/**/out/ +!**/src/test/**/out/ + +### Eclipse ### +.apt_generated .classpath - -# Java annotation processor (APT) .factorypath - -# PDT-specific (PHP Development Tools) -.buildpath - -# sbteclipse plugin -.target - -# Tern plugin -.tern-project - -# TeXlipse plugin -.texlipse - -# STS (Spring Tool Suite) +.project +.settings .springBeans - -# Code Recommenders -.recommenders/ - - -# maven build directory -target/ - -# lib directory (containing the burp jar) -lib/ - -# ignore debug output -*log +.sts4-cache +bin/ +!**/src/main/**/bin/ +!**/src/test/**/bin/ + +### NetBeans ### +/nbproject/private/ +/nbbuild/ +/dist/ +/nbdist/ +/.nb-gradle/ + +### VS Code ### +.vscode/ + +### Mac OS ### +.DS_Store +/target/ diff --git a/BappManifest.bmf b/BappManifest.bmf index 6e2e178..7990ebf 100644 --- a/BappManifest.bmf +++ b/BappManifest.bmf @@ -6,7 +6,7 @@ ScreenVersion: 2.2 SerialVersion: 21 MinPlatformVersion: 0 ProOnly: False -Author: Oussama Zgheb & Mathias Vetsch +Author: Oussama Zgheb ShortDescription: Enables Burp to decode and manipulate JSON web tokens. EntryPoint: target/JWT4B-jar-with-dependencies.jar BuildCommand: mvn package -DskipTests=true -Dmaven.javadoc.skip=true -B diff --git a/README.md b/README.md index a5abe6a..856cb6d 100644 --- a/README.md +++ b/README.md @@ -26,12 +26,6 @@ A config file will be created under "%user.home%\.JWT4B\config.json" with the fo "resetEditor": true, "highlightColor": "blue", "interceptComment": "Contains a JWT", - "jwtKeywords": [ - "Authorization: Bearer", - "Authorization: bearer", - "authorization: Bearer", - "authorization: bearer" - ], "tokenKeywords": [ "id_token", "ID_TOKEN", @@ -48,9 +42,9 @@ Note: If resetEditor is set to false, all options such as the re-singing and alg ## Building your own version (with Eclipse) 1. Clone repository and create new Eclipse Java Project -2. Rightclick -> Configure -> Convert to Maven Project (downloading all required libraries) +2. Rightclick -> Configure -> Convert to Gradle Project (downloading all required libraries) 3. Open Burp -> Extensions -> APIs -> Save interface files -> Copy all files to JWT4B\src\burp -4. Export runnable fat JAR including libraries +4. Gradle -> build jar 5. Load the JAR in Burp through the Extender Tab -> Extensions -> Add (Good to know: CTRL+Click on a extension to reload it) # Installation from BApp Store diff --git a/build.gradle b/build.gradle new file mode 100644 index 0000000..2c229fc --- /dev/null +++ b/build.gradle @@ -0,0 +1,52 @@ +plugins { + id 'java' + id 'eclipse' +} + +group = 'org.example' +version = '1.0-SNAPSHOT' + +repositories { + maven { url "https://artifact.swissre.com/artifactory/internal" } + maven { url "https://artifact.swissre.com/artifactory/external" } + maven { url "https://artifact.swissre.com/artifactory/thirdparty" } + + //mavenCentral() +} + +dependencies { + compileOnly "net.portswigger.burp.extensions:montoya-api:${extender_version}" + compileOnly 'org.projectlombok:lombok:1.18.32' + annotationProcessor 'org.projectlombok:lombok:1.18.32' + + + implementation ( + 'com.auth0:java-jwt:3.11.0', + 'commons-codec:commons-codec:1.15', + 'com.fifesoft:rsyntaxtextarea:3.1.1', + 'commons-lang:commons-lang:2.6', + 'com.fasterxml.jackson.core:jackson-databind:2.12.7.1', + 'com.eclipsesource.minimal-json:minimal-json:0.9.4', + ) + + testImplementation( + "net.portswigger.burp.extensions:montoya-api:${extender_version}", + "org.junit.jupiter:junit-jupiter-engine:${junit_version}", + "org.junit.jupiter:junit-jupiter-params:${junit_version}", + 'org.assertj:assertj-core:3.22.0', + 'org.mockito:mockito-core:5.8.0', + 'org.apache.commons:commons-text:1.10.0' + ) +} + +test { + useJUnitPlatform() +} + +jar { + manifest { + attributes 'implementation-version': "${version}" + } + duplicatesStrategy = DuplicatesStrategy.EXCLUDE + from { configurations.runtimeClasspath.collect { it.isDirectory() ? it : zipTree(it) } } +} \ No newline at end of file diff --git a/gradle.properties b/gradle.properties new file mode 100644 index 0000000..e4ed64b --- /dev/null +++ b/gradle.properties @@ -0,0 +1,2 @@ +extender_version = 2023.12.1 +junit_version = 5.11.0-M2 \ No newline at end of file diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 0000000..249e583 Binary files /dev/null and b/gradle/wrapper/gradle-wrapper.jar differ diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 0000000..b189e60 --- /dev/null +++ b/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,6 @@ +#Sat May 25 16:59:06 CEST 2024 +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-8.5-bin.zip +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists diff --git a/gradlew b/gradlew new file mode 100644 index 0000000..1b6c787 --- /dev/null +++ b/gradlew @@ -0,0 +1,234 @@ +#!/bin/sh + +# +# Copyright © 2015-2021 the original authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +############################################################################## +# +# Gradle start up script for POSIX generated by Gradle. +# +# Important for running: +# +# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is +# noncompliant, but you have some other compliant shell such as ksh or +# bash, then to run this script, type that shell name before the whole +# command line, like: +# +# ksh Gradle +# +# Busybox and similar reduced shells will NOT work, because this script +# requires all of these POSIX shell features: +# * functions; +# * expansions «$var», «${var}», «${var:-default}», «${var+SET}», +# «${var#prefix}», «${var%suffix}», and «$( cmd )»; +# * compound commands having a testable exit status, especially «case»; +# * various built-in commands including «command», «set», and «ulimit». +# +# Important for patching: +# +# (2) This script targets any POSIX shell, so it avoids extensions provided +# by Bash, Ksh, etc; in particular arrays are avoided. +# +# The "traditional" practice of packing multiple parameters into a +# space-separated string is a well documented source of bugs and security +# problems, so this is (mostly) avoided, by progressively accumulating +# options in "$@", and eventually passing that to Java. +# +# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS, +# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly; +# see the in-line comments for details. +# +# There are tweaks for specific operating systems such as AIX, CygWin, +# Darwin, MinGW, and NonStop. +# +# (3) This script is generated from the Groovy template +# https://github.com/gradle/gradle/blob/master/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt +# within the Gradle project. +# +# You can find Gradle at https://github.com/gradle/gradle/. +# +############################################################################## + +# Attempt to set APP_HOME + +# Resolve links: $0 may be a link +app_path=$0 + +# Need this for daisy-chained symlinks. +while + APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path + [ -h "$app_path" ] +do + ls=$( ls -ld "$app_path" ) + link=${ls#*' -> '} + case $link in #( + /*) app_path=$link ;; #( + *) app_path=$APP_HOME$link ;; + esac +done + +APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit + +APP_NAME="Gradle" +APP_BASE_NAME=${0##*/} + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD=maximum + +warn () { + echo "$*" +} >&2 + +die () { + echo + echo "$*" + echo + exit 1 +} >&2 + +# OS specific support (must be 'true' or 'false'). +cygwin=false +msys=false +darwin=false +nonstop=false +case "$( uname )" in #( + CYGWIN* ) cygwin=true ;; #( + Darwin* ) darwin=true ;; #( + MSYS* | MINGW* ) msys=true ;; #( + NONSTOP* ) nonstop=true ;; +esac + +CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar + + +# Determine the Java command to use to start the JVM. +if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD=$JAVA_HOME/jre/sh/java + else + JAVACMD=$JAVA_HOME/bin/java + fi + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +else + JAVACMD=java + which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." +fi + +# Increase the maximum file descriptors if we can. +if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then + case $MAX_FD in #( + max*) + MAX_FD=$( ulimit -H -n ) || + warn "Could not query maximum file descriptor limit" + esac + case $MAX_FD in #( + '' | soft) :;; #( + *) + ulimit -n "$MAX_FD" || + warn "Could not set maximum file descriptor limit to $MAX_FD" + esac +fi + +# Collect all arguments for the java command, stacking in reverse order: +# * args from the command line +# * the main class name +# * -classpath +# * -D...appname settings +# * --module-path (only if needed) +# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. + +# For Cygwin or MSYS, switch paths to Windows format before running java +if "$cygwin" || "$msys" ; then + APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) + CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" ) + + JAVACMD=$( cygpath --unix "$JAVACMD" ) + + # Now convert the arguments - kludge to limit ourselves to /bin/sh + for arg do + if + case $arg in #( + -*) false ;; # don't mess with options #( + /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath + [ -e "$t" ] ;; #( + *) false ;; + esac + then + arg=$( cygpath --path --ignore --mixed "$arg" ) + fi + # Roll the args list around exactly as many times as the number of + # args, so each arg winds up back in the position where it started, but + # possibly modified. + # + # NB: a `for` loop captures its iteration list before it begins, so + # changing the positional parameters here affects neither the number of + # iterations, nor the values presented in `arg`. + shift # remove old arg + set -- "$@" "$arg" # push replacement arg + done +fi + +# Collect all arguments for the java command; +# * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of +# shell script including quotes and variable substitutions, so put them in +# double quotes to make sure that they get re-expanded; and +# * put everything else in single quotes, so that it's not re-expanded. + +set -- \ + "-Dorg.gradle.appname=$APP_BASE_NAME" \ + -classpath "$CLASSPATH" \ + org.gradle.wrapper.GradleWrapperMain \ + "$@" + +# Use "xargs" to parse quoted args. +# +# With -n1 it outputs one arg per line, with the quotes and backslashes removed. +# +# In Bash we could simply go: +# +# readarray ARGS < <( xargs -n1 <<<"$var" ) && +# set -- "${ARGS[@]}" "$@" +# +# but POSIX shell has neither arrays nor command substitution, so instead we +# post-process each arg (as a line of input to sed) to backslash-escape any +# character that might be a shell metacharacter, then use eval to reverse +# that process (while maintaining the separation between arguments), and wrap +# the whole thing up as a single "set" statement. +# +# This will of course break if any of these variables contains a newline or +# an unmatched quote. +# + +eval "set -- $( + printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | + xargs -n1 | + sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | + tr '\n' ' ' + )" '"$@"' + +exec "$JAVACMD" "$@" diff --git a/gradlew.bat b/gradlew.bat new file mode 100644 index 0000000..107acd3 --- /dev/null +++ b/gradlew.bat @@ -0,0 +1,89 @@ +@rem +@rem Copyright 2015 the original author or authors. +@rem +@rem Licensed under the Apache License, Version 2.0 (the "License"); +@rem you may not use this file except in compliance with the License. +@rem You may obtain a copy of the License at +@rem +@rem https://www.apache.org/licenses/LICENSE-2.0 +@rem +@rem Unless required by applicable law or agreed to in writing, software +@rem distributed under the License is distributed on an "AS IS" BASIS, +@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +@rem See the License for the specific language governing permissions and +@rem limitations under the License. +@rem + +@if "%DEBUG%" == "" @echo off +@rem ########################################################################## +@rem +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables with windows NT shell +if "%OS%"=="Windows_NT" setlocal + +set DIRNAME=%~dp0 +if "%DIRNAME%" == "" set DIRNAME=. +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Resolve any "." and ".." in APP_HOME to make it shorter. +for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if "%ERRORLEVEL%" == "0" goto execute + +echo. +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto execute + +echo. +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:execute +@rem Setup the command line + +set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar + + +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* + +:end +@rem End local scope for the variables with windows NT shell +if "%ERRORLEVEL%"=="0" goto mainEnd + +:fail +rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of +rem the _cmd.exe /c_ return code! +if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 +exit /b 1 + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega diff --git a/pom.xml b/pom.xml deleted file mode 100644 index 65817f3..0000000 --- a/pom.xml +++ /dev/null @@ -1,113 +0,0 @@ - - 4.0.0 - JWT4B - JWT4B - 0.0.4-SNAPSHOT - - - 5.8.2 - - - - - com.auth0 - java-jwt - 3.11.0 - - - commons-codec - commons-codec - 1.15 - - - com.fifesoft - rsyntaxtextarea - 3.1.1 - - - commons-lang - commons-lang - 2.6 - - - com.fasterxml.jackson.core - jackson-databind - 2.12.7.1 - - - com.eclipsesource.minimal-json - minimal-json - 0.9.4 - - - net.portswigger.burp.extender - burp-extender-api - 1.7.13 - - - - - org.junit.jupiter - junit-jupiter-engine - ${junit.version} - test - - - org.junit.jupiter - junit-jupiter-params - ${junit.version} - test - - - org.assertj - assertj-core - 3.22.0 - test - - - - - src - test - ${project.name} - - - maven-compiler-plugin - 3.8.1 - - 20 - 20 - - jar-with-dependencies - - UTF-8 - - - - maven-surefire-plugin - 3.0.0-M5 - - - maven-assembly-plugin - 3.0.0 - - - jar-with-dependencies - - - - - make-assembly - package - - single - - - - - - - - \ No newline at end of file diff --git a/settings.gradle b/settings.gradle new file mode 100644 index 0000000..f04d044 --- /dev/null +++ b/settings.gradle @@ -0,0 +1,2 @@ +rootProject.name = 'JWT4B_Montoya' + diff --git a/src/app/JWT4B.java b/src/app/JWT4B.java deleted file mode 100644 index 1dc56a1..0000000 --- a/src/app/JWT4B.java +++ /dev/null @@ -1,8 +0,0 @@ -package app; - -public class JWT4B { - public static void main(String[] args){ - // This solves the export warning concerning not finding a main method - System.out.println("This JAR is meant to be run through BURP extender"); - } -} \ No newline at end of file diff --git a/src/app/controllers/ContextMenuController.java b/src/app/controllers/ContextMenuController.java deleted file mode 100644 index ac382f9..0000000 --- a/src/app/controllers/ContextMenuController.java +++ /dev/null @@ -1,65 +0,0 @@ -package app.controllers; - -import java.util.LinkedList; -import java.util.List; - -import javax.swing.JMenuItem; - -import app.helpers.Output; -import burp.IContextMenuFactory; -import burp.IContextMenuInvocation; -import burp.IHttpRequestResponse; -import model.Strings; - -import static org.apache.commons.lang.StringUtils.isNotEmpty; - -// This controller handles the right-click context option "Send selected text to JWT4B Tab to decode -// which is available in the Raw view of the HTTP history tab - -public class ContextMenuController implements IContextMenuFactory { - private final JWTSuiteTabController jstC; - - public ContextMenuController(JWTSuiteTabController jstC) { - this.jstC = jstC; - } - - @Override - public List createMenuItems(IContextMenuInvocation invocation) { - String selectedText = selectedText(invocation); - List menuItems = new LinkedList<>(); - - if (isNotEmpty(selectedText)) { - JMenuItem item = new JMenuItem(Strings.contextMenuString); - item.addActionListener(e -> jstC.contextActionSendJWTtoSuiteTab(selectedText, true)); - - menuItems.add(item); - } - - return menuItems; - } - - private static String selectedText(IContextMenuInvocation invocation) { - int[] selection = invocation.getSelectionBounds(); - - if (selection == null) { // only if user currently is in an input field - return ""; - } - - IHttpRequestResponse ihrr = invocation.getSelectedMessages()[0]; - byte iContext = invocation.getInvocationContext(); - - switch (iContext) { - case IContextMenuInvocation.CONTEXT_MESSAGE_EDITOR_REQUEST: - case IContextMenuInvocation.CONTEXT_MESSAGE_VIEWER_REQUEST: - return new String(ihrr.getRequest()).substring(selection[0], selection[1]); - - case IContextMenuInvocation.CONTEXT_MESSAGE_EDITOR_RESPONSE: - case IContextMenuInvocation.CONTEXT_MESSAGE_VIEWER_RESPONSE: - return new String(ihrr.getResponse()).substring(selection[0], selection[1]); - - default: - Output.outputError("This context menu case (" + invocation + ") has not been covered yet!"); - return ""; - } - } -} diff --git a/src/app/controllers/HighLightController.java b/src/app/controllers/HighLightController.java deleted file mode 100644 index a567c24..0000000 --- a/src/app/controllers/HighLightController.java +++ /dev/null @@ -1,41 +0,0 @@ -package app.controllers; - -import app.helpers.Config; -import app.tokenposition.ITokenPosition; -import burp.IBurpExtenderCallbacks; -import burp.IExtensionHelpers; -import burp.IHttpListener; -import burp.IHttpRequestResponse; - -// This controller handles the highlighting of entries in the HTTP history tab - -public class HighLightController implements IHttpListener { - - private final IExtensionHelpers helpers; - - public HighLightController(IBurpExtenderCallbacks callbacks) { - this.helpers = callbacks.getHelpers(); - } - - @Override - public void processHttpMessage(int toolFlag, boolean isRequest, IHttpRequestResponse httpRequestResponse) { - byte[] content = isRequest ? httpRequestResponse.getRequest() : httpRequestResponse.getResponse(); - boolean containsJWT = ITokenPosition.findTokenPositionImplementation(content, isRequest, helpers) != null; - if (containsJWT) { - if (!Config.interceptComment.equals("")) { - markRequestResponseWithComment(httpRequestResponse, Config.interceptComment); - } - markRequestResponseWithColor(httpRequestResponse); - } - } - - private void markRequestResponseWithComment(IHttpRequestResponse httpRequestResponse, String comment) { - httpRequestResponse.setComment(comment); - } - - private void markRequestResponseWithColor(IHttpRequestResponse httpRequestResponse) { - if (!Config.highlightColor.equals("none")) { - httpRequestResponse.setHighlight(Config.highlightColor); - } - } -} diff --git a/src/app/controllers/JWTInterceptTabController.java b/src/app/controllers/JWTInterceptTabController.java deleted file mode 100644 index 5d19970..0000000 --- a/src/app/controllers/JWTInterceptTabController.java +++ /dev/null @@ -1,505 +0,0 @@ -package app.controllers; - -import java.awt.Component; -import java.awt.FileDialog; -import java.awt.Frame; -import java.awt.event.ActionListener; -import java.awt.event.KeyEvent; -import java.awt.event.KeyListener; -import java.nio.charset.StandardCharsets; -import java.security.interfaces.RSAPublicKey; -import java.util.Base64; -import java.util.List; -import java.util.Objects; - -import javax.swing.JCheckBox; -import javax.swing.JComboBox; -import javax.swing.SwingUtilities; -import javax.swing.event.DocumentEvent; -import javax.swing.event.DocumentListener; - -import org.apache.commons.codec.binary.Hex; - -import com.auth0.jwt.algorithms.Algorithm; -import com.eclipsesource.json.Json; -import com.eclipsesource.json.JsonObject; - -import app.algorithm.AlgorithmWrapper; -import app.helpers.Config; -import app.helpers.DelayedDocumentListener; -import app.helpers.KeyHelper; -import app.helpers.O365; -import app.helpers.Output; -import app.helpers.PublicKeyBroker; -import app.tokenposition.ITokenPosition; -import burp.IBurpExtenderCallbacks; -import burp.IExtensionHelpers; -import burp.IMessageEditorTab; -import gui.JWTInterceptTab; -import model.CustomJWToken; -import model.JWTInterceptModel; -import model.Settings; -import model.Strings; -import model.TimeClaim; - - -// used in the proxy intercept and repeater tabs -public class JWTInterceptTabController implements IMessageEditorTab { - - private final JWTInterceptModel jwtIM; - private final JWTInterceptTab jwtST; - private final IExtensionHelpers helpers; - private ITokenPosition tokenPosition; - private boolean resignOnType; - private boolean randomKey; - private boolean chooseSignature; - private boolean recalculateSignature; - private String algAttackMode; - private boolean cveAttackMode; - private boolean edited; - private String originalSignature; - private boolean addMetaHeader; - private static final String HEX_MARKER = "0x"; - - - public JWTInterceptTabController(IBurpExtenderCallbacks callbacks, JWTInterceptModel jwIM, JWTInterceptTab jwtST) { - this.jwtIM = jwIM; - this.jwtST = jwtST; - this.helpers = callbacks.getHelpers(); - createAndRegisterActionListeners(jwtST); - } - - @Override - public boolean isEnabled(byte[] content, boolean isRequest) { - return ITokenPosition.findTokenPositionImplementation(content, isRequest, helpers) != null; - } - - @Override - public void setMessage(byte[] content, boolean isRequest) { - edited = false; - tokenPosition = ITokenPosition.findTokenPositionImplementation(content, isRequest, helpers); - boolean messageContainsJWT = tokenPosition != null; - if (messageContainsJWT) { - String token = tokenPosition.getToken(); - CustomJWToken cJWT = new CustomJWToken(token); - jwtIM.setOriginalJWToken(new CustomJWToken(token)); - jwtIM.setOriginalJWT(token); - List tcl = cJWT.getTimeClaimList(); - jwtIM.setTimeClaims(tcl); - jwtIM.setJwToken(cJWT); - originalSignature = cJWT.getSignature(); - jwtIM.setcFW(tokenPosition.getcFW()); - jwtST.updateSetView(Config.resetEditor); - algAttackMode = null; - if (Config.resetEditor) { - jwtST.getNoneAttackComboBox().setSelectedIndex(0); - } - } else { - jwtST.updateSetView(true); - } - } - - @Override - public byte[] getMessage() { - // see https://github.com/PortSwigger/example-custom-editor-tab/blob/master/java/BurpExtender.java#L119 - boolean nothingChanged = - !edited && !recalculateSignature && !randomKey && !chooseSignature && algAttackMode == null && !cveAttackMode; - if (nothingChanged) { - return tokenPosition.getMessage(); - } - clearError(); - radioButtonChanged(true, false, false, false, false); - jwtST.getCVEAttackCheckBox().setSelected(false); - replaceTokenInMessage(); - addLogHeadersToRequest(); - return tokenPosition.getMessage(); - } - - private void cveAttackChanged() { - edited = true; - JCheckBox jcb = jwtST.getCVEAttackCheckBox(); - cveAttackMode = jcb.isSelected(); - jwtST.getNoneAttackComboBox().setEnabled(!cveAttackMode); - jwtST.getRdbtnDontModify().setEnabled(!cveAttackMode); - jwtST.getRdbtnOriginalSignature().setEnabled(!cveAttackMode); - jwtST.getRdbtnRandomKey().setEnabled(!cveAttackMode); - jwtST.getRdbtnRecalculateSignature().setEnabled(!cveAttackMode); - jwtST.setKeyFieldState(!cveAttackMode); - jwtST.getCVECopyBtn().setVisible(cveAttackMode); - - if (cveAttackMode) { - jwtST.getRdbtnDontModify().setSelected(true); - jwtST.getRdbtnOriginalSignature().setSelected(false); - jwtST.getRdbtnRandomKey().setSelected(false); - jwtST.getRdbtnRecalculateSignature().setSelected(false); - CustomJWToken token = jwtIM.getJwToken(); - String headerJSON = token.getHeaderJson(); - JsonObject headerJSONObj = Json.parse(headerJSON).asObject(); - headerJSONObj.set("alg", "RS256"); - JsonObject jwk = new JsonObject(); - jwk.add("kty", "RSA"); - jwk.add("kid", "jwt4b@portswigger.net"); - jwk.add("use", "sig"); - RSAPublicKey pk = KeyHelper.loadCVEAttackPublicKey(); - jwk.add("n", Base64.getUrlEncoder().encodeToString(pk.getPublicExponent().toByteArray())); - jwk.add("e", Base64.getUrlEncoder().encodeToString(pk.getModulus().toByteArray())); - headerJSONObj.add("jwk", jwk); - token.setHeaderJson(headerJSONObj.toString()); - try { - Algorithm algo = AlgorithmWrapper.getSignerAlgorithm(token.getAlgorithm(), Config.cveAttackModePrivateKey); - token.calculateAndSetSignature(algo); - reflectChangeToView(token, true); - } catch (Exception e) { - reportError("Failed to sign when using cve attack mode - " + e.getMessage()); - } - } else { - jwtST.setKeyFieldValue(""); - jwtST.setKeyFieldState(false); - CustomJWToken customJWToken = new CustomJWToken(jwtIM.getOriginalJWT()); - jwtIM.setJwToken(customJWToken); - jwtST.getNoneAttackComboBox().setSelectedIndex(0); - reflectChangeToView(jwtIM.getJwToken(), true); - } - } - - private void signKeyChange() { - jwtIM.setJWTSignatureKey(jwtST.getKeyFieldValue()); - try { - if (jwtIM.getJWTKey() != null && jwtIM.getJWTKey().length() > 0 && jwtST.getKeyField().isEnabled()) { - String key = getKeyWithHexDetection(jwtIM); - Output.output("Signing with manually entered key '" + key + "' (" + jwtIM.getJWTKey() + ")"); - CustomJWToken token = ReadableTokenFormat.getTokenFromView(jwtST); - - if (Config.o365Support && O365.isO365Request(token, token.getAlgorithm())) { - O365.handleO365(key, token); - reflectChangeToView(token, false); - } else { - Algorithm algo = AlgorithmWrapper.getSignerAlgorithm(token.getAlgorithm(), key); - token.calculateAndSetSignature(algo); - reflectChangeToView(token, false); - } - clearError(); - } - } catch (Exception e) { - int len = 8; - String key = jwtST.getKeyFieldValue(); - key = key.length() > len ? key.substring(0, len) + "..." : key; - reportError("Cannot sign with key " + key + " - " + e.getMessage()); - } - } - - - private String getKeyWithHexDetection(JWTInterceptModel jwtIM) { - String key = jwtIM.getJWTKey(); - if (key.startsWith(HEX_MARKER)) { - try { - key = key.substring(2); - byte[] bytes = Hex.decodeHex(key); - key = new String(bytes, StandardCharsets.ISO_8859_1); - } catch (Exception e) { - key = jwtIM.getJWTKey(); - } - } - return key; - } - - private void algAttackChanged() { - edited = true; - JComboBox jCB = jwtST.getNoneAttackComboBox(); - switch (jCB.getSelectedIndex()) { - default: - case 0: - algAttackMode = null; - break; - case 1: - algAttackMode = "none"; - break; - case 2: - algAttackMode = "None"; - break; - case 3: - algAttackMode = "nOnE"; - break; - case 4: - algAttackMode = "NONE"; - break; - } - - CustomJWToken token = jwtIM.getJwToken(); - String header = token.getHeaderJson(); - if (algAttackMode == null) { - Output.output("Resetting alg attack mode - " + jwtIM.getOriginalJWToken().getAlgorithm()); - token.setHeaderJson(header.replace(token.getAlgorithm(), jwtIM.getOriginalJWToken().getAlgorithm())); - token.setSignature(originalSignature); - jwtST.getRdbtnDontModify().setSelected(true); - radioButtonChanged(true, false, false, false, false); - jwtST.setRadiosState(true); - jwtST.setKeyFieldState(true); - } else { - jwtST.getRdbtnDontModify().setSelected(true); - jwtST.setRadiosState(false); - jwtST.setKeyFieldState(false); - token.setSignature(""); - token.setHeaderJson(header.replace(token.getAlgorithm(), algAttackMode)); - } - reflectChangeToView(token, true); - } - - private void radioButtonChanged(boolean cDM, boolean cRK, boolean cOS, boolean cRS, boolean cCS) { - clearError(); - resignOnType = !cDM && !cOS; - boolean oldRandomKey = randomKey; - edited = true; - addMetaHeader = false; - boolean dontModifySignature = jwtST.getRdbtnDontModify().isSelected(); - randomKey = jwtST.getRdbtnRandomKey().isSelected(); - boolean keepOriginalSignature = jwtST.getRdbtnOriginalSignature().isSelected(); - recalculateSignature = jwtST.getRdbtnRecalculateSignature().isSelected(); - chooseSignature = jwtST.getRdbtnChooseSignature().isSelected(); - - jwtST.setKeyFieldState(!keepOriginalSignature && !dontModifySignature && !randomKey && !chooseSignature); - if (keepOriginalSignature) { - CustomJWToken origSignatureToken = jwtIM.getJwToken().setSignature(originalSignature); - jwtIM.setJwToken(origSignatureToken); - jwtIM.setJWTSignatureKey(""); - jwtST.setKeyFieldValue(""); - jwtST.updateSetView(false); - } else if (dontModifySignature) { - jwtIM.setJWTSignatureKey(""); - jwtST.setKeyFieldValue(""); - } else if (randomKey) { - addMetaHeader = true; - if (!oldRandomKey) { - generateRandomKey(); - } - } else if (cCS) { - FileDialog dialog = new FileDialog((Frame) null, "Select File to Open"); - dialog.setMode(FileDialog.LOAD); - dialog.setVisible(true); - if (dialog.getFile() != null) { - String file = dialog.getDirectory() + dialog.getFile(); - Output.output(file + " chosen."); - String chosen = Strings.filePathToString(file); - // TODO error will be redirected to Output.outputError, but make sure this case will be shown in UI too - jwtIM.setJWTSignatureKey(chosen); - jwtST.setKeyFieldValue(chosen); - jwtST.updateSetView(false); - } - } else if ((recalculateSignature || chooseSignature)) { - if (recalculateSignature) { - String cleanKey = KeyHelper.cleanKey(jwtST.getKeyFieldValue()); - jwtIM.setJWTSignatureKey(cleanKey); - } - Algorithm algo; - try { - if (jwtIM.getJWTKey().length() > 0) { - CustomJWToken token = jwtIM.getJwToken(); - Output.output("Recalculating Signature with Secret - '" + jwtIM.getJWTKey() + "'"); - algo = AlgorithmWrapper.getSignerAlgorithm(token.getAlgorithm(), jwtIM.getJWTKey()); - token.calculateAndSetSignature(algo); - reflectChangeToView(token, true); - addMetaHeader = true; - } - } catch (IllegalArgumentException e) { - reportError("Exception while recalculating signature - " + e.getMessage()); - } - } - } - - private void clearError() { - jwtIM.setProblemDetail(""); - jwtST.setProblemLbl(""); - } - - private void reportError(String error) { - Output.outputError(error); - jwtIM.setProblemDetail(error); - jwtST.setProblemLbl(jwtIM.getProblemDetail()); - } - - private void generateRandomKey() { - SwingUtilities.invokeLater(() -> { - try { - CustomJWToken token = ReadableTokenFormat.getTokenFromView(jwtST); - String generatedRandomKey = KeyHelper.getRandomKey(token.getAlgorithm()); - Output.output("Generating Random Key for Signature Calculation: " + generatedRandomKey); - jwtIM.setJWTSignatureKey(generatedRandomKey); - Algorithm algo = AlgorithmWrapper.getSignerAlgorithm(token.getAlgorithm(), generatedRandomKey); - token.calculateAndSetSignature(algo); - jwtIM.setJwToken(token); - jwtST.setKeyFieldValue(generatedRandomKey); - jwtST.updateSetView(false); - } catch (Exception e) { - reportError("Exception during random key generation & signing: " + e.getMessage()); - } - }); - } - - private void replaceTokenInMessage() { - CustomJWToken token; - if (!jwtInUIisValid()) { - Output.outputError("Wont replace JWT as invalid"); - edited = false; - return; - } - try { - token = ReadableTokenFormat.getTokenFromView(jwtST); - Output.output("Replacing token: " + token.getToken()); - // token may be null, if it is invalid JSON, if so, don't try changing anything - if (token.getToken() != null) { - this.tokenPosition.setMessage(this.tokenPosition.replaceToken(token.getToken())); - } - } catch (Exception e) { - // TODO is this visible to user? - reportError("Could not replace token in message: " + e.getMessage()); - } - } - - private void addLogHeadersToRequest() { - if (addMetaHeader) { - this.tokenPosition.cleanJWTHeaders(); - this.tokenPosition.addHeader(Strings.JWTHeaderInfo); - this.tokenPosition.addHeader(Strings.JWTHeaderPrefix + "SIGNER-KEY " + jwtIM.getJWTKey()); - if (PublicKeyBroker.publicKey != null) { - this.tokenPosition.addHeader(Strings.JWTHeaderPrefix + "SIGNER-PUBLIC-KEY " + PublicKeyBroker.publicKey); - PublicKeyBroker.publicKey = null; - } - } - } - - private void reflectChangeToView(CustomJWToken token, boolean updateKey) { - jwtIM.setJwToken(token); - jwtST.updateSetView(false, updateKey); - } - - private void handleJWTAreaTyped() { - if (jwtInUIisValid()) { - clearError(); - } else { - // TODO determine error more accurately, what exactly is wrong with the jwt - reportError("invalid JWT"); - return; - } - if (resignOnType) { - Output.output("Recalculating signature as key typed"); - CustomJWToken token = null; - try { - token = ReadableTokenFormat.getTokenFromView(jwtST); - } catch (Exception e) { - reportError("JWT can't be parsed - " + e.getMessage()); - } - if (jwtIM.getJWTKey().length() == 0) { - reportError("Can't resign with an empty key"); - } - try { - Algorithm algo = AlgorithmWrapper.getSignerAlgorithm(Objects.requireNonNull(token).getAlgorithm(), - jwtIM.getJWTKey()); - token.calculateAndSetSignature(algo); - jwtIM.setJwToken(token); - jwtST.getJwtSignatureArea().setText(jwtIM.getJwToken().getSignature()); - } catch (Exception e) { - reportError("Could not resign: " + e.getMessage()); - } - } - } - - public boolean jwtInUIisValid() { - boolean valid = false; - try { - CustomJWToken tokenFromView = ReadableTokenFormat.getTokenFromView(jwtST); - if (tokenFromView.getHeaderJsonNode().get("alg") != null) { - valid = CustomJWToken.isValidJWT(tokenFromView.getToken()); - } - } catch (Exception ignored) { - // ignored - } - return valid; - } - - @Override - public String getTabCaption() { - return Settings.TAB_NAME; - } - - @Override - public Component getUiComponent() { - return jwtST; - } - - @Override - public boolean isModified() { - - return edited; - } - - @Override - public byte[] getSelectedData() { - return jwtST.getSelectedData().getBytes(); - } - - private void createAndRegisterActionListeners(JWTInterceptTab jwtST) { - - KeyListener editedKeyListener = new KeyListener() { - - @Override - public void keyTyped(KeyEvent arg0) { - } - - @Override - public void keyReleased(KeyEvent arg0) { - } - - @Override - public void keyPressed(KeyEvent arg0) { - edited = true; - } - }; - - jwtST.getJwtPayloadArea().addKeyListener(editedKeyListener); - - ActionListener dontModifyListener = e -> radioButtonChanged(true, false, false, false, false); - ActionListener randomKeyListener = e -> radioButtonChanged(false, true, false, false, false); - ActionListener originalSignatureListener = e -> radioButtonChanged(false, false, true, false, false); - ActionListener recalculateSignatureListener = e -> radioButtonChanged(false, false, false, true, false); - ActionListener chooseSignatureListener = e -> radioButtonChanged(false, false, false, false, true); - ActionListener algAttackListener = e -> algAttackChanged(); - ActionListener cveAttackListener = e -> cveAttackChanged(); - - DocumentListener jwtKeyChanged = new DelayedDocumentListener(new DocumentListener() { - - @Override - public void insertUpdate(DocumentEvent e) { - signKeyChange(); - } - - @Override - public void removeUpdate(DocumentEvent e) { - signKeyChange(); - } - - @Override - public void changedUpdate(DocumentEvent e) { - } - }); - - KeyListener jwtAreaTyped = new KeyListener() { - - @Override - public void keyTyped(KeyEvent e) { - } - - @Override - public void keyPressed(KeyEvent e) { - } - - @Override - public void keyReleased(KeyEvent e) { - handleJWTAreaTyped(); - } - }; - - jwtST.registerActionListeners(dontModifyListener, randomKeyListener, originalSignatureListener, - recalculateSignatureListener, chooseSignatureListener, algAttackListener, cveAttackListener, jwtKeyChanged, - jwtAreaTyped); - } -} diff --git a/src/app/controllers/JWTSuiteTabController.java b/src/app/controllers/JWTSuiteTabController.java deleted file mode 100644 index 5c1fd50..0000000 --- a/src/app/controllers/JWTSuiteTabController.java +++ /dev/null @@ -1,166 +0,0 @@ -package app.controllers; - -import java.awt.Component; -import java.util.List; - -import javax.swing.JTabbedPane; -import javax.swing.event.DocumentEvent; -import javax.swing.event.DocumentListener; - -import app.algorithm.AlgorithmWrapper; -import com.auth0.jwt.JWT; -import com.auth0.jwt.JWTVerifier; -import com.auth0.jwt.exceptions.InvalidClaimException; -import com.auth0.jwt.exceptions.JWTVerificationException; -import com.auth0.jwt.exceptions.SignatureVerificationException; -import com.auth0.jwt.interfaces.DecodedJWT; - -import app.helpers.Output; -import burp.ITab; -import gui.JWTSuiteTab; -import model.CustomJWToken; -import model.JWTSuiteTabModel; -import model.Settings; -import model.Strings; -import model.TimeClaim; - -// used to provide the standalone suite tab after "User Options" -public class JWTSuiteTabController implements ITab { - - private final JWTSuiteTabModel jwtSTM; - private final JWTSuiteTab jwtST; - - public JWTSuiteTabController(final JWTSuiteTabModel jwtSTM, final JWTSuiteTab jwtST) { - this.jwtSTM = jwtSTM; - this.jwtST = jwtST; - - createAndRegisterActionListeners(jwtSTM, jwtST); - } - - // This method was copied from - // https://support.portswigger.net/customer/portal/questions/16743551-burp-extension-get-focus-on-tab-after-custom-menu-action - public void selectJWTSuiteTab() { - Component current = this.getUiComponent(); - do { - current = current.getParent(); - } while (!(current instanceof JTabbedPane)); - - JTabbedPane tabPane = (JTabbedPane) current; - for (int i = 0; i < tabPane.getTabCount(); i++) { - if (tabPane.getTitleAt(i).equals(this.getTabCaption())) - tabPane.setSelectedIndex(i); - } - } - - public void contextActionSendJWTtoSuiteTab(String jwts, boolean fromContextMenu) { - jwts = jwts.replace("Authorization:", ""); - jwts = jwts.replace("Bearer", ""); - jwts = jwts.replace("Set-Cookie: ", ""); - jwts = jwts.replace("Cookie: ", ""); - jwts = jwts.replaceAll("\\s", ""); - jwtSTM.setJwtInput(jwts); - try { - CustomJWToken jwt = new CustomJWToken(jwts); - List tcl = jwt.getTimeClaimList(); - jwtSTM.setTimeClaims(tcl); - jwtSTM.setJwtJSON(ReadableTokenFormat.getReadableFormat(jwt)); - } catch (Exception e) { - Output.outputError("JWT Context Action: " + e.getMessage()); - } - if (fromContextMenu) { - // Reset View and Select - jwtSTM.setJwtKey(""); - selectJWTSuiteTab(); - } else { - // Since we changed the JWT, we need to check the Key/Signature too - contextActionKey(jwtSTM.getJwtKey()); - } - jwtST.updateSetView(); - } - - public void contextActionKey(String key) { - jwtSTM.setJwtKey(key); - jwtSTM.setVerificationResult(""); - try { - CustomJWToken token = new CustomJWToken(jwtSTM.getJwtInput()); - String curAlgo = token.getAlgorithm(); - JWTVerifier verifier = JWT.require(AlgorithmWrapper.getVerifierAlgorithm(curAlgo, key)).build(); - DecodedJWT test = verifier.verify(token.getToken()); - jwtSTM.setJwtSignatureColor(Settings.getValidColor()); - jwtSTM.setVerificationLabel(Strings.verificationValid); - test.getAlgorithm(); - } catch (JWTVerificationException e) { - Output.output("Verification failed (" + e.getMessage() + ")"); - jwtSTM.setVerificationResult(e.getMessage()); - - if (e instanceof SignatureVerificationException) { - jwtSTM.setJwtSignatureColor(Settings.getInvalidColor()); - jwtSTM.setVerificationLabel(Strings.verificationInvalidSignature); - } else if (e instanceof InvalidClaimException) { - jwtSTM.setJwtSignatureColor(Settings.getProblemColor()); - jwtSTM.setVerificationLabel(Strings.verificationInvalidClaim); - } else { - jwtSTM.setJwtSignatureColor(Settings.getProblemColor()); - jwtSTM.setVerificationLabel(Strings.verificationError); - } - - } catch (IllegalArgumentException e) { - Output.output("Verification failed (" + e.getMessage() + ")"); - jwtSTM.setJwtSignatureColor(Settings.getProblemColor()); - jwtSTM.setVerificationResult(e.getMessage()); - jwtSTM.setVerificationLabel(Strings.verificationInvalidKey); - } - jwtST.updateSetView(); - } - - @Override - public String getTabCaption() { - return Settings.TAB_NAME; - } - - @Override - public Component getUiComponent() { - return jwtST; - } - - private void createAndRegisterActionListeners(final JWTSuiteTabModel jwtSTM, final JWTSuiteTab jwtST) { - DocumentListener jwtDocInputListener = new DocumentListener() { - - @Override - public void removeUpdate(DocumentEvent e) { - jwtSTM.setJwtInput(jwtST.getJWTInput()); - contextActionSendJWTtoSuiteTab(jwtSTM.getJwtInput(), false); - } - - @Override - public void insertUpdate(DocumentEvent e) { - jwtSTM.setJwtInput(jwtST.getJWTInput()); - contextActionSendJWTtoSuiteTab(jwtSTM.getJwtInput(), false); - } - - @Override - public void changedUpdate(DocumentEvent e) { - } - }; - DocumentListener jwtDocKeyListener = new DocumentListener() { - - @Override - public void removeUpdate(DocumentEvent e) { - jwtSTM.setJwtKey(jwtST.getKeyInput()); - contextActionKey(jwtSTM.getJwtKey()); - } - - @Override - public void insertUpdate(DocumentEvent e) { - jwtSTM.setJwtKey(jwtST.getKeyInput()); - contextActionKey(jwtSTM.getJwtKey()); - } - - @Override - public void changedUpdate(DocumentEvent e) { - } - }; - - jwtST.registerDocumentListener(jwtDocInputListener, jwtDocKeyListener); - } -} diff --git a/src/app/controllers/JWTTabController.java b/src/app/controllers/JWTTabController.java deleted file mode 100644 index 0b7a667..0000000 --- a/src/app/controllers/JWTTabController.java +++ /dev/null @@ -1,185 +0,0 @@ -package app.controllers; - -import java.awt.Component; -import java.util.ArrayList; -import java.util.List; -import java.util.Objects; - -import javax.swing.event.DocumentEvent; -import javax.swing.event.DocumentListener; - -import com.auth0.jwt.JWT; -import com.auth0.jwt.JWTVerifier; -import com.auth0.jwt.exceptions.InvalidClaimException; -import com.auth0.jwt.exceptions.JWTVerificationException; -import com.auth0.jwt.exceptions.SignatureVerificationException; -import com.auth0.jwt.interfaces.DecodedJWT; - -import app.algorithm.AlgorithmType; -import app.algorithm.AlgorithmWrapper; -import app.helpers.Output; -import app.tokenposition.ITokenPosition; -import burp.IBurpExtenderCallbacks; -import burp.IExtensionHelpers; -import burp.IMessageEditorTab; -import gui.JWTViewTab; -import model.CustomJWToken; -import model.JWTTabModel; -import model.Settings; -import model.Strings; -import model.TimeClaim; - -// view to check JWTs such as in the HTTP history -public class JWTTabController implements IMessageEditorTab { - - private final IExtensionHelpers helpers; - private byte[] message; - private ITokenPosition tokenPosition; - private final ArrayList modelStateList = new ArrayList(); - private byte[] content; - private final JWTTabModel jwtTM; - private final JWTViewTab jwtVT; - - public JWTTabController(IBurpExtenderCallbacks callbacks, final JWTTabModel jwtTM, final JWTViewTab jwtVT) { - this.helpers = callbacks.getHelpers(); - this.jwtTM = jwtTM; - this.jwtVT = jwtVT; - - DocumentListener documentListener = new DocumentListener() { - - @Override - public void removeUpdate(DocumentEvent arg0) { - jwtTM.setKey(jwtVT.getKeyValue()); - checkKey(jwtTM.getKey()); - } - - @Override - public void insertUpdate(DocumentEvent arg0) { - jwtTM.setKey(jwtVT.getKeyValue()); - checkKey(jwtTM.getKey()); - } - - @Override - public void changedUpdate(DocumentEvent arg0) { - jwtTM.setKey(jwtVT.getKeyValue()); - checkKey(jwtTM.getKey()); - } - }; - - jwtVT.registerDocumentListener(documentListener); - } - - @Override - public boolean isEnabled(byte[] content, boolean isRequest) { - this.content = content; - return ITokenPosition.findTokenPositionImplementation(content, isRequest, helpers) != null; - } - - @Override - public void setMessage(byte[] content, boolean isRequest) { - this.message = content; - try { - tokenPosition = ITokenPosition.findTokenPositionImplementation(content, isRequest, helpers); - jwtTM.setJWT(Objects.requireNonNull(tokenPosition).getToken()); - } catch (Exception e) { - Output.outputError("Exception setting message: " + e.getMessage()); - } - CustomJWToken jwt = new CustomJWToken(jwtTM.getJWT()); - jwtTM.setJWTJSON(ReadableTokenFormat.getReadableFormat(jwt)); - List tcl = jwt.getTimeClaimList(); - jwtTM.setTimeClaims(tcl); - if (tokenPosition != null) { - jwtTM.setcFW(tokenPosition.getcFW()); - } - - JWTTabModel current = new JWTTabModel(jwtTM.getKey(), content); - int containsIndex = modelStateList.indexOf(current); - - // we know this request, load it - if (containsIndex != -1) { - JWTTabModel knownModel = modelStateList.get(containsIndex); - jwtTM.setKey(knownModel.getKey()); - jwtTM.setVerificationColor(knownModel.getVerificationColor()); - jwtTM.setVerificationLabel(knownModel.getVerificationLabel()); - // we haven't seen this request yet, add it and set the view to - // default - } else { - modelStateList.add(current); - jwtTM.setVerificationColor(Settings.COLOR_UNDEFINED); - jwtTM.setVerificationResult(""); - jwtTM.setKey(""); - } - AlgorithmType algoType = AlgorithmWrapper.getTypeOf(getCurrentAlgorithm()); - jwtVT.updateSetView(algoType); - } - - @Override - public byte[] getMessage() { - return message; - } - - @Override - public boolean isModified() { - return false; - } - - public void checkKey(String key) { - jwtTM.setVerificationResult(""); - String curAlgo = getCurrentAlgorithm(); - AlgorithmType algoType = AlgorithmWrapper.getTypeOf(getCurrentAlgorithm()); - try { - JWTVerifier verifier = JWT.require(AlgorithmWrapper.getVerifierAlgorithm(curAlgo, key)).build(); - DecodedJWT test = verifier.verify(jwtTM.getJWT()); - jwtTM.setVerificationLabel(Strings.verificationValid); - jwtTM.setVerificationColor(Settings.getValidColor()); - test.getAlgorithm(); - jwtVT.updateSetView(algoType); - } catch (JWTVerificationException e) { - if (e instanceof SignatureVerificationException) { - jwtTM.setVerificationColor(Settings.getInvalidColor()); - jwtTM.setVerificationLabel(Strings.verificationInvalidSignature); - } else if (e instanceof InvalidClaimException) { - jwtTM.setVerificationColor(Settings.getProblemColor()); - jwtTM.setVerificationLabel(Strings.verificationInvalidClaim); - } else { - jwtTM.setVerificationColor(Settings.getProblemColor()); - jwtTM.setVerificationLabel(Strings.verificationError); - } - jwtTM.setVerificationResult(e.getMessage()); - jwtVT.updateSetView(algoType); - } catch (IllegalArgumentException e) { - jwtTM.setVerificationResult(e.getMessage()); - jwtTM.setVerificationLabel(Strings.verificationInvalidKey); - jwtTM.setVerificationColor(Settings.getProblemColor()); - jwtVT.updateSetView(algoType); - } - JWTTabModel current = new JWTTabModel(key, content); - int containsIndex = modelStateList.indexOf(current); - if (containsIndex != -1) { // we know this request, update the viewstate - JWTTabModel knownState = modelStateList.get(containsIndex); - knownState.setKeyValueAndHash(key, current.getHashCode()); - knownState.setVerificationResult(jwtTM.getVerificationLabel()); - knownState.setVerificationColor(jwtTM.getVerificationColor()); - } - } - - @Override - public byte[] getSelectedData() { - return jwtVT.getSelectedData().getBytes(); - } - - @Override - public String getTabCaption() { - return Settings.TAB_NAME; - } - - @Override - public Component getUiComponent() { - return this.jwtVT; - } - - public String getCurrentAlgorithm() { - return new CustomJWToken(jwtTM.getJWT()).getAlgorithm(); - } - -} diff --git a/src/app/controllers/ReadableTokenFormat.java b/src/app/controllers/ReadableTokenFormat.java deleted file mode 100644 index b02597a..0000000 --- a/src/app/controllers/ReadableTokenFormat.java +++ /dev/null @@ -1,60 +0,0 @@ -package app.controllers; - -import static com.eclipsesource.json.WriterConfig.PRETTY_PRINT; - -import static org.apache.commons.lang.StringUtils.isBlank; - -import com.eclipsesource.json.Json; -import com.eclipsesource.json.JsonValue; - -import app.helpers.Output; -import gui.JWTInterceptTab; -import model.CustomJWToken; - -public class ReadableTokenFormat { - - ReadableTokenFormat() { - - } - - private static final String NEW_LINE = System.getProperty("line.separator"); - private static final String TITTLE_HEADERS = "Headers = "; - private static final String TITLE_PAYLOAD = NEW_LINE + NEW_LINE + "Payload = "; - private static final String TITLE_SIGNATURE = NEW_LINE + NEW_LINE + "Signature = "; - - public static String getReadableFormat(CustomJWToken token) { - - return TITTLE_HEADERS + jsonBeautify(token.getHeaderJson()) + TITLE_PAYLOAD + jsonBeautify(token.getPayloadJson()) - + TITLE_SIGNATURE + "\"" + token.getSignature() + "\""; - } - - public static String jsonBeautify(String input) { - if (isBlank(input)) { - return ""; - } - - try { - JsonValue value = Json.parse(input); - return value.toString(PRETTY_PRINT); - } catch (RuntimeException e) { - Output.outputError("Exception beautifying JSON: " + e.getMessage()); - return input; - } - } - - public static CustomJWToken getTokenFromView(JWTInterceptTab jwtST) { - String header = jwtST.getJwtHeaderArea().getText(); - String payload = jwtST.getJwtPayloadArea().getText(); - String signature = jwtST.getJwtSignatureArea().getText(); - return new CustomJWToken(header, payload, signature); - } - - public static class InvalidTokenFormat extends Exception { - - private static final long serialVersionUID = 1L; - - public InvalidTokenFormat(String message) { - super(message); - } - } -} diff --git a/src/app/helpers/CookieFlagWrapper.java b/src/app/helpers/CookieFlagWrapper.java deleted file mode 100644 index 932535b..0000000 --- a/src/app/helpers/CookieFlagWrapper.java +++ /dev/null @@ -1,52 +0,0 @@ -package app.helpers; - -public class CookieFlagWrapper { - - private final boolean secureFlag; - private final boolean httpOnlyFlag; - private final boolean isCookie; - - public CookieFlagWrapper(boolean isCookie, boolean secureFlag, boolean httpOnlyFlag) { - this.isCookie = isCookie; - this.secureFlag = secureFlag; - this.httpOnlyFlag = httpOnlyFlag; - } - - public boolean isCookie() { - return isCookie; - } - - public boolean hasHttpOnlyFlag() { - if (isCookie) { - return httpOnlyFlag; - } - return false; - } - - public boolean hasSecureFlag() { - if (isCookie) { - return secureFlag; - } - return false; - } - - public String toHTMLString() { - if (!isCookie) { - return ""; - } - String returnString = "
"; - if (!hasSecureFlag()) { - returnString += "No secure flag set. Token may be transmitted by HTTP.
"; - } else { - returnString += "Secure Flag set.
"; - } - if (!hasHttpOnlyFlag()) { - returnString += "No HttpOnly flag set. Token may accessed by JavaScript (XSS)."; - } else { - returnString += "HttpOnly Flag set."; - } - returnString += "
"; - return returnString; - } - -} diff --git a/src/app/helpers/KeyHelper.java b/src/app/helpers/KeyHelper.java deleted file mode 100644 index db9729a..0000000 --- a/src/app/helpers/KeyHelper.java +++ /dev/null @@ -1,142 +0,0 @@ -package app.helpers; - -import static org.apache.commons.lang.StringUtils.isNotEmpty; - -import java.security.Key; -import java.security.KeyFactory; -import java.security.KeyPair; -import java.security.KeyPairGenerator; -import java.security.NoSuchAlgorithmException; -import java.security.PrivateKey; -import java.security.PublicKey; -import java.security.interfaces.RSAPublicKey; -import java.security.spec.EncodedKeySpec; -import java.security.spec.PKCS8EncodedKeySpec; -import java.security.spec.X509EncodedKeySpec; - -import javax.crypto.Mac; -import javax.crypto.spec.SecretKeySpec; - -import org.apache.commons.codec.binary.Base64; -import org.apache.commons.lang.RandomStringUtils; - -import app.algorithm.AlgorithmType; -import app.algorithm.AlgorithmWrapper; - -public class KeyHelper { - - KeyHelper() { - - } - - private static final String[] KEY_BEGIN_MARKERS = new String[]{"-----BEGIN PUBLIC KEY-----", - "-----BEGIN CERTIFICATE-----"}; - private static final String[] KEY_END_MARKERS = new String[]{"-----END PUBLIC KEY-----", "-----END CERTIFICATE-----"}; - public static final String HMAC_SHA_256 = "HmacSHA256"; - - public static String getRandomKey(String algorithm) { - AlgorithmType algorithmType = AlgorithmWrapper.getTypeOf(algorithm); - - if (algorithmType.equals(AlgorithmType.SYMMETRIC)) { - return RandomStringUtils.randomAlphanumeric(6); - } - if (algorithmType.equals(AlgorithmType.ASYMMETRIC) && algorithm.startsWith("RS")) { - try { - KeyPair keyPair = KeyPairGenerator.getInstance("RSA").generateKeyPair(); - - PublicKeyBroker.publicKey = Base64.encodeBase64String(keyPair.getPublic().getEncoded()); - return Base64.encodeBase64String(keyPair.getPrivate().getEncoded()); - } catch (NoSuchAlgorithmException e) { - Output.outputError(e.getMessage()); - } - } - if (algorithmType.equals(AlgorithmType.ASYMMETRIC) && algorithm.startsWith("ES")) { - try { - KeyPair keyPair = KeyPairGenerator.getInstance("EC").generateKeyPair(); - return Base64.encodeBase64String(keyPair.getPrivate().getEncoded()); - } catch (NoSuchAlgorithmException e) { - Output.outputError(e.getMessage()); - } - } - throw new IllegalArgumentException( - "Cannot get random key of provided algorithm as it does not seem valid HS, RS or ES"); - } - - public static PrivateKey generatePrivateKeyFromString(String key, String algorithm) { - PrivateKey privateKey = null; - if (isNotEmpty(key)) { - key = cleanKey(key); - try { - byte[] keyByteArray = Base64.decodeBase64(key); - KeyFactory kf = KeyFactory.getInstance(algorithm); - EncodedKeySpec keySpec = new PKCS8EncodedKeySpec(keyByteArray); - privateKey = kf.generatePrivate(keySpec); - } catch (Exception e) { - Output.outputError( - "Error generating private key with input string '" + key + "' and algorithm '" + algorithm + "' - " - + e.getMessage() + " - "); - } - } - return privateKey; - } - - public static String cleanKey(String key) { - for (String keyBeginMarker : KEY_BEGIN_MARKERS) { - key = key.replace(keyBeginMarker, ""); - } - for (String keyEndMarker : KEY_END_MARKERS) { - key = key.replace(keyEndMarker, ""); - } - key = key.replaceAll("\\s+", "").replaceAll("\\r+", "").replaceAll("\\n+", ""); - - return key; - } - - public static RSAPublicKey loadCVEAttackPublicKey() { - String publicPEM = KeyHelper.cleanKey(Config.cveAttackModePublicKey); - KeyFactory kf; - try { - kf = KeyFactory.getInstance("RSA"); - X509EncodedKeySpec keySpecX509 = new X509EncodedKeySpec(java.util.Base64.getDecoder().decode(publicPEM)); - return (RSAPublicKey) kf.generatePublic(keySpecX509); - } catch (Exception e) { - Output.outputError("Could not load public key - " + e.getMessage()); - e.printStackTrace(); - } - return null; - } - - private static PublicKey generatePublicKeyFromString(String key, String algorithm) { - PublicKey publicKey = null; - if (isNotEmpty(key)) { - key = cleanKey(key); - byte[] keyByteArray = java.util.Base64.getDecoder().decode(key); - try { - KeyFactory kf = KeyFactory.getInstance(algorithm); - EncodedKeySpec keySpec = new X509EncodedKeySpec(keyByteArray); - publicKey = kf.generatePublic(keySpec); - } catch (Exception e) { - Output.outputError(e.getMessage()); - } - } - return publicKey; - } - - public static Key getKeyInstance(String key, String algorithm, boolean isPrivate) { - return isPrivate ? generatePrivateKeyFromString(key, algorithm) : generatePublicKeyFromString(key, algorithm); - } - - public static byte[] calcHmacSha256(byte[] secretKey, byte[] message) { - byte[] hmacSha256 = null; - try { - Mac mac = Mac.getInstance(HMAC_SHA_256); - SecretKeySpec secretKeySpec = new SecretKeySpec(secretKey, HMAC_SHA_256); - mac.init(secretKeySpec); - hmacSha256 = mac.doFinal(message); - } catch (Exception e) { - Output.outputError("Exception during " + HMAC_SHA_256 + ": " + e.getMessage()); - } - return hmacSha256; - } -} - diff --git a/src/app/helpers/KeyValuePair.java b/src/app/helpers/KeyValuePair.java deleted file mode 100644 index 09842e4..0000000 --- a/src/app/helpers/KeyValuePair.java +++ /dev/null @@ -1,32 +0,0 @@ -package app.helpers; - -public class KeyValuePair { - - private String name; - private String value; - - public KeyValuePair(String name, String value) { - this.setName(name); - this.setValue(value); - } - - public String getName() { - return name; - } - - public String getNameAsParam() { - return name + "="; - } - - public void setName(String name) { - this.name = name; - } - - public String getValue() { - return value; - } - - public void setValue(String value) { - this.value = value; - } -} diff --git a/src/app/helpers/O365.java b/src/app/helpers/O365.java deleted file mode 100644 index bf066c1..0000000 --- a/src/app/helpers/O365.java +++ /dev/null @@ -1,56 +0,0 @@ -package app.helpers; - -import java.nio.charset.StandardCharsets; -import java.security.MessageDigest; -import java.security.NoSuchAlgorithmException; -import java.util.Base64; - -import org.apache.commons.codec.binary.Hex; - -import com.auth0.jwt.algorithms.Algorithm; -import com.fasterxml.jackson.databind.JsonNode; - -import app.algorithm.AlgorithmWrapper; -import model.CustomJWToken; - -public class O365 { - - O365() { - - } - - public static boolean isO365Request(CustomJWToken token, String tokenalgo) { - return token.getHeaderJsonNode().get("ctx") != null && tokenalgo.toUpperCase() - .contains(AlgorithmWrapper.HS256.name()); - } - - public static void handleO365(String key, CustomJWToken token) throws NoSuchAlgorithmException { - String label = "AzureAD-SecureConversation"; - String ctx = token.getHeaderJsonNode().get("ctx").asText(); - byte[] ctxbytes = Base64.getDecoder().decode(ctx); - - JsonNode kdfVer = token.getHeaderJsonNode().get("kdf_ver"); - boolean tokenCreatedWithKDFv2 = kdfVer != null && kdfVer.asInt() == 2; - if (tokenCreatedWithKDFv2) { - byte[] fullctxbytes = new byte[24 + token.getPayloadJson().replace(" ", "").replace("\n", "").length()]; - System.arraycopy(ctxbytes, 0, fullctxbytes, 0, 24); - System.arraycopy(token.getPayloadJson().replace(" ", "").replace("\n", "").getBytes(StandardCharsets.ISO_8859_1), - 0, fullctxbytes, 24, token.getPayloadJson().replace(" ", "").replace("\n", "").length()); - MessageDigest digest = MessageDigest.getInstance("SHA-256"); - ctxbytes = digest.digest(fullctxbytes); - } - - byte[] newArr = new byte[4 + label.getBytes(StandardCharsets.UTF_8).length + 1 + ctxbytes.length + 4]; - System.arraycopy(new byte[]{(byte) 0x00, 0x00, 0x00, 0x01}, 0, newArr, 0, 4); - System.arraycopy(label.getBytes(StandardCharsets.UTF_8), 0, newArr, 4, 26); - System.arraycopy(new byte[]{(byte) 0x00}, 0, newArr, 30, 1); - System.arraycopy(ctxbytes, 0, newArr, 31, ctxbytes.length); - System.arraycopy(new byte[]{(byte) 0x00, 0x00, 0x01, 0x00}, 0, newArr, newArr.length - 4, 4); - byte[] keyData = key.getBytes(StandardCharsets.ISO_8859_1); - byte[] hmacSha256 = KeyHelper.calcHmacSha256(keyData, newArr); - - Algorithm algo = AlgorithmWrapper.getSignerAlgorithm(token.getAlgorithm(), hmacSha256); - Output.output("Signing with MS O365 derived key: " + Hex.encodeHexString(hmacSha256)); - token.calculateAndSetSignature(algo); - } -} diff --git a/src/app/helpers/Output.java b/src/app/helpers/Output.java deleted file mode 100644 index 9733e32..0000000 --- a/src/app/helpers/Output.java +++ /dev/null @@ -1,35 +0,0 @@ -package app.helpers; - -import java.io.OutputStream; -import java.io.PrintWriter; -import java.text.DateFormat; -import java.text.SimpleDateFormat; -import java.util.Calendar; -import java.util.Date; -import java.util.TimeZone; - -public class Output { - private static final DateFormat DATE_FORMAT = new SimpleDateFormat("HH:mm:ss.SSS"); - - private static PrintWriter stdout = new PrintWriter(System.out, true); - private static PrintWriter stderr = new PrintWriter(System.err, true); - - public static void initialise(OutputStream outputStream, OutputStream errorStream) { - stdout = new PrintWriter(outputStream, true); - stderr = new PrintWriter(errorStream, true); - } - - public static void output(String string) { - write(stdout, string); - } - - public static void outputError(String string) { - write(stderr, string); - } - - private static void write(PrintWriter writer, String string) { - Date cal = Calendar.getInstance(TimeZone.getDefault()).getTime(); - String msg = DATE_FORMAT.format(cal.getTime()) + " | " + string; - writer.println(msg); - } -} diff --git a/src/app/helpers/PublicKeyBroker.java b/src/app/helpers/PublicKeyBroker.java deleted file mode 100644 index f574dcb..0000000 --- a/src/app/helpers/PublicKeyBroker.java +++ /dev/null @@ -1,14 +0,0 @@ -package app.helpers; - -/** - * Created by mvetsch on 24.04.2017. - */ -public class PublicKeyBroker { - - PublicKeyBroker() { - - } - - // This hack is used to get public Key from the Random Key generator to the controller. Just for logging - public static String publicKey; -} diff --git a/src/app/helpers/TokenChecker.java b/src/app/helpers/TokenChecker.java deleted file mode 100644 index d7001b1..0000000 --- a/src/app/helpers/TokenChecker.java +++ /dev/null @@ -1,39 +0,0 @@ -package app.helpers; - -import org.apache.commons.lang.StringUtils; - -import com.auth0.jwt.JWT; -import com.auth0.jwt.interfaces.DecodedJWT; - -public class TokenChecker { - - public static final String JWT_ALLOWED_CHARS_REGEXP = "[A-Za-z0-9+/=_-]+"; - - public static boolean isValidJWT(String jwt) { - int dotCount = StringUtils.countMatches(jwt, "."); - if (dotCount != 2) { - return false; - } - - jwt = jwt.trim(); - if (StringUtils.contains(jwt, " ")) { - return false; - } - - for (String part : StringUtils.split(jwt, ".")) { - if (!part.matches(JWT_ALLOWED_CHARS_REGEXP)) { - return false; - } - } - - try { - DecodedJWT decoded = JWT.decode(jwt); - decoded.getAlgorithm(); - return true; - } catch (Exception ignored) { - // ignored - } - - return false; - } -} diff --git a/src/app/tokenposition/AuthorizationBearerHeader.java b/src/app/tokenposition/AuthorizationBearerHeader.java deleted file mode 100644 index a77342f..0000000 --- a/src/app/tokenposition/AuthorizationBearerHeader.java +++ /dev/null @@ -1,55 +0,0 @@ -package app.tokenposition; - -import java.util.List; - -import app.helpers.Config; -import model.CustomJWToken; - -// finds and replaces JWT's in authorization headers -public class AuthorizationBearerHeader extends ITokenPosition { - - private String selectedKeyword; - private Integer headerIndex; - private final List headers; - - public AuthorizationBearerHeader(List headers, String ignored) { - this.headers = headers; - } - - public boolean positionFound() { - for (int counter = 0; counter < headers.size(); counter++) { - if (headerContainsKeyWordAndIsJWT(headers.get(counter), Config.jwtKeywords)) { - this.headerIndex = counter; - return true; - } - } - return false; - } - - private boolean headerContainsKeyWordAndIsJWT(String header, List jwtKeywords) { - for (String keyword : jwtKeywords) { - if (header.startsWith(keyword)) { - String jwt = header.replace(keyword, "").trim(); - if (CustomJWToken.isValidJWT(jwt)) { - this.selectedKeyword = keyword; - return true; - } - } - } - return false; - } - - public String getToken() { - if (this.headerIndex == null) { - return ""; - } - return headers.get(this.headerIndex).substring(this.selectedKeyword.length() + 1); - } - - public byte[] replaceToken(String newToken) { - if (positionFound()) { // updating headerIndex - headers.set(this.headerIndex, this.selectedKeyword + " " + newToken); - } - return getHelpers().buildHttpMessage(headers, getBody()); - } -} diff --git a/src/app/tokenposition/Body.java b/src/app/tokenposition/Body.java deleted file mode 100644 index 10e0d4e..0000000 --- a/src/app/tokenposition/Body.java +++ /dev/null @@ -1,139 +0,0 @@ -package app.tokenposition; - -import java.util.List; -import java.util.regex.Pattern; - -import org.apache.commons.lang.StringUtils; - -import com.eclipsesource.json.Json; -import com.eclipsesource.json.JsonObject; - -import app.helpers.KeyValuePair; -import app.helpers.Output; -import app.helpers.TokenChecker; - -//finds and replaces JWT's in HTTP bodies -public class Body extends ITokenPosition { - - private String token; - private boolean found = false; - private String body; - - public Body(List ignored, String body) { - this.body = body; - } - - @Override - public boolean positionFound() { - KeyValuePair postJWT = getJWTFromBody(); - if (postJWT != null) { - found = true; - token = postJWT.getValue(); - return true; - } - return false; - } - - public KeyValuePair getJWTFromBody() { - KeyValuePair ret; - if ((ret = getJWTFromBodyWithParameters()) != null) { - return ret; - } else if ((ret = getJWTFromBodyWithJson()) != null) { - return ret; - } else { - return getJWTFromBodyWithoutParametersOrJSON(); - } - } - - private KeyValuePair getJWTFromBodyWithoutParametersOrJSON() { - String[] split = StringUtils.split(body); - for (String strg : split) { - if (TokenChecker.isValidJWT(strg)) { - return new KeyValuePair("", strg); - } - } - return null; - } - - private KeyValuePair getJWTFromBodyWithJson() { - JsonObject obj; - try { - if (body.length() < 2) { - return null; - } - obj = Json.parse(body).asObject(); - } catch (Exception e) { - return null; - } - return lookForJwtInJsonObject(obj); - } - - private KeyValuePair lookForJwtInJsonObject(JsonObject object) { - KeyValuePair rec; - for (String name : object.names()) { - if (object.get(name).isString()) { - if (TokenChecker.isValidJWT(object.get(name).asString())) { - return new KeyValuePair(name, object.get(name).asString().trim()); - } - } else if (object.get(name).isObject()) { - if ((rec = lookForJwtInJsonObject(object.get(name).asObject())) != null) { - return rec; - } - } - } - return null; - } - - - private KeyValuePair getJWTFromBodyWithParameters() { - int from = 0; - int index = body.contains("&") ? body.indexOf("&") : body.length(); - int parameterCount = StringUtils.countMatches(body, "&") + 1; - - for (int i = 0; i < parameterCount; i++) { - String parameter = body.substring(from, index); - parameter = parameter.replace("&", ""); - - String[] parameterSplit = parameter.split(Pattern.quote("=")); - if (parameterSplit.length > 1) { - String name = parameterSplit[0]; - String value = parameterSplit[1]; - if (TokenChecker.isValidJWT(value)) { - return new KeyValuePair(name, value); - } - - from = index; - index = body.indexOf("&", index + 1); - if (index == -1) { - index = body.length(); - } - } - } - return null; - } - - @Override - public String getToken() { - return found ? token : ""; - } - - @Override - public byte[] replaceToken(String newToken) { - body = replaceTokenImpl(newToken, body); - return getHelpers().buildHttpMessage(getHeaders(), body.getBytes()); - } - - public String replaceTokenImpl(String newToken, String body) { - boolean replaced = false; - KeyValuePair postJWT = getJWTFromBody(); - if (postJWT != null) { - String oldBody = body; - body = body.replace(postJWT.getValue(), newToken); - replaced = !oldBody.equals(body); - } - if (!replaced) { - Output.outputError("Could not replace token in post body."); - } - return body; - } -} diff --git a/src/app/tokenposition/Cookie.java b/src/app/tokenposition/Cookie.java deleted file mode 100644 index e5454a7..0000000 --- a/src/app/tokenposition/Cookie.java +++ /dev/null @@ -1,121 +0,0 @@ -package app.tokenposition; - -import java.util.List; -import java.util.regex.Pattern; - -import org.apache.commons.lang.StringUtils; - -import app.helpers.CookieFlagWrapper; -import app.helpers.TokenChecker; - -//finds and replaces JWT's in cookies -public class Cookie extends ITokenPosition { - - public static final String SET_COOKIE_HEADER = "Set-Cookie: "; - public static final String COOKIE_HEADER = "Cookie: "; - private boolean found; - private String token; - private List headers; - private CookieFlagWrapper cFW = null; - - public Cookie(List headers, String body) { - this.headers = headers; - } - - @Override - public boolean positionFound() { - String jwt = findJWTInHeaders(headers); - if (jwt != null) { - found = true; - token = jwt; - return true; - } - return false; - } - - // finds the first jwt in the set-cookie or cookie header(s) - public String findJWTInHeaders(List headers) { - // defaulting - cFW = new CookieFlagWrapper(false, false, false); - - for (String header : headers) { - if (header.startsWith(SET_COOKIE_HEADER)) { - String cookie = header.replace(SET_COOKIE_HEADER, ""); - if (cookie.length() > 1 && cookie.contains("=")) { - String value = cookie.split(Pattern.quote("="))[1]; - int flagMarker = value.indexOf(";"); - if (flagMarker != -1) { - value = value.substring(0, flagMarker); - cFW = new CookieFlagWrapper(true, cookie.toLowerCase().contains("; secure"), - cookie.toLowerCase().contains("; httponly")); - } else { - cFW = new CookieFlagWrapper(true, false, false); - } - TokenChecker.isValidJWT(value); - if (TokenChecker.isValidJWT(value)) { - found = true; - token = value; - return value; - } - } - } - if (header.startsWith(COOKIE_HEADER)) { - String cookieHeader = header.replace(COOKIE_HEADER, ""); - cookieHeader = cookieHeader.endsWith(";") ? cookieHeader : cookieHeader + ";"; - int from = 0; - int index = cookieHeader.indexOf(";"); - int cookieCount = StringUtils.countMatches(cookieHeader, ";"); - for (int i = 0; i < cookieCount; i++) { - String cookie = cookieHeader.substring(from, index); - cookie = cookie.replace(";", ""); - String[] cvp = cookie.split(Pattern.quote("=")); - String value = cvp.length == 2 ? cvp[1] : ""; - if (TokenChecker.isValidJWT(value)) { - found = true; - token = value; - return value; - } - from = index; - index = cookieHeader.indexOf(";", index + 1); - if (index == -1) { - index = cookieHeader.length(); - } - } - } - } - return null; - } - - @Override - public String getToken() { - return found ? token : ""; - } - - @Override - public byte[] replaceToken(String newToken) { - headers = replaceTokenInHeader(newToken, headers); - return getHelpers().buildHttpMessage(headers, getBody()); - } - - public List replaceTokenInHeader(String newToken, List headers) { - int i = 0; - Integer pos = null; - String replacedHeader = ""; - - for (String header : headers) { - if (header.contains(token)) { - pos = i; - replacedHeader = header.replace(token, newToken); - } - i++; - } - if (pos != null) { - headers.set(pos, replacedHeader); - } - return headers; - } - - public CookieFlagWrapper getcFW() { - return cFW; - } -} diff --git a/src/app/tokenposition/Dummy.java b/src/app/tokenposition/Dummy.java deleted file mode 100644 index 4e6631d..0000000 --- a/src/app/tokenposition/Dummy.java +++ /dev/null @@ -1,21 +0,0 @@ -package app.tokenposition; - -public class Dummy extends ITokenPosition { - - public static final String CURLY_BRACKET_B64 = "e30=."; - - @Override - public boolean positionFound() { - return false; - } - - @Override - public String getToken() { - return CURLY_BRACKET_B64 + CURLY_BRACKET_B64; - } - - @Override - public byte[] replaceToken(String newToken) { - return (CURLY_BRACKET_B64 + CURLY_BRACKET_B64).getBytes(); - } -} diff --git a/src/app/tokenposition/ITokenPosition.java b/src/app/tokenposition/ITokenPosition.java deleted file mode 100644 index 0870b1f..0000000 --- a/src/app/tokenposition/ITokenPosition.java +++ /dev/null @@ -1,167 +0,0 @@ -package app.tokenposition; - -import java.util.ArrayList; -import java.util.Arrays; -import java.util.List; - -import app.helpers.CookieFlagWrapper; -import app.helpers.Output; -import burp.IExtensionHelpers; -import burp.IRequestInfo; -import burp.IResponseInfo; -import model.Strings; - -public abstract class ITokenPosition { - - protected IExtensionHelpers helpers; - protected byte[] message; - protected boolean isRequest; - - public abstract boolean positionFound(); - - public abstract String getToken(); - - public abstract byte[] replaceToken(String newToken); - - private static CookieFlagWrapper cookieFlagWrap; - - public void setMessage(byte[] message, boolean isRequest) { - this.message = message; - this.isRequest = isRequest; - } - - public void setMessage(byte[] message) { - this.message = message; - } - - public void setHelpers(IExtensionHelpers helpers) { - this.helpers = helpers; - } - - protected List getHeaders() { - if (message == null) { - return new ArrayList(); - } - if (isRequest) { - IRequestInfo requestInfo = helpers.analyzeRequest(message); - return requestInfo.getHeaders(); - } else { - IResponseInfo responseInfo = helpers.analyzeResponse(message); - return responseInfo.getHeaders(); - } - } - - public static ITokenPosition findTokenPositionImplementation(byte[] content, - boolean isRequest, - IExtensionHelpers helpers) { - List> implementations = Arrays.asList(AuthorizationBearerHeader.class, - PostBody.class, Cookie.class, Body.class); - if (content == null) { - return new Dummy(); - } - for (Class implClass : implementations) { - try { - List headers; - int bodyOffset; - if (isRequest) { - IRequestInfo requestInfo = helpers.analyzeRequest(content); - headers = requestInfo.getHeaders(); - bodyOffset = requestInfo.getBodyOffset(); - } else { - IResponseInfo responseInfo = helpers.analyzeResponse(content); - headers = responseInfo.getHeaders(); - bodyOffset = responseInfo.getBodyOffset(); - - } - String body = new String(Arrays.copyOfRange(content, bodyOffset, content.length)); - ITokenPosition impl = (ITokenPosition) implClass.getConstructors()[0].newInstance(headers, body); - - impl.setHelpers(helpers); - impl.setMessage(content, isRequest); - if (impl.positionFound()) { - if (impl instanceof Cookie) { - cookieFlagWrap = ((Cookie) impl).getcFW(); - } else { - cookieFlagWrap = new CookieFlagWrapper(false, false, false); - } - return impl; - } - } catch (Exception e) { - // sometimes 'isEnabled' is called in order to build the views - // before an actual request / response passes through - in that case - // it is not worth reporting - if (!e.getMessage().equals("Request cannot be null") && !e.getMessage().equals("1")) { - Output.outputError(e.getMessage()); - } - return null; - } - } - return null; - } - - protected int getBodyOffset() { - if (isRequest) { - IRequestInfo requestInfo = helpers.analyzeRequest(message); - return requestInfo.getBodyOffset(); - } else { - IResponseInfo responseInfo = helpers.analyzeResponse(message); - return responseInfo.getBodyOffset(); - } - } - - protected byte[] getBody() { - return Arrays.copyOfRange(message, getBodyOffset(), message.length); - } - - protected IExtensionHelpers getHelpers() { - return helpers; - } - - public void addHeader(String headerToAdd) { - List headers; - int offset; - if (isRequest) { - IRequestInfo requestInfo = helpers.analyzeRequest(message); - headers = requestInfo.getHeaders(); - offset = requestInfo.getBodyOffset(); - } else { - IResponseInfo responseInfo = helpers.analyzeResponse(message); - headers = responseInfo.getHeaders(); - offset = responseInfo.getBodyOffset(); - } - headers.add(headerToAdd); - this.message = helpers.buildHttpMessage(headers, Arrays.copyOfRange(message, offset, message.length)); - } - - public void cleanJWTHeaders() { - List headers; - List toOverwriteHeaders = new ArrayList<>(); - int offset; - - if (isRequest) { - IRequestInfo requestInfo = helpers.analyzeRequest(message); - headers = requestInfo.getHeaders(); - offset = requestInfo.getBodyOffset(); - } else { - IResponseInfo responseInfo = helpers.analyzeResponse(message); - headers = responseInfo.getHeaders(); - offset = responseInfo.getBodyOffset(); - } - - for (String header : headers) { - if (header.startsWith(Strings.JWTHeaderPrefix)) { - toOverwriteHeaders.add(header); - } - } - headers.removeAll(toOverwriteHeaders); - this.message = helpers.buildHttpMessage(headers, Arrays.copyOfRange(message, offset, message.length)); - } - - public byte[] getMessage() { - return this.message; - } - - public CookieFlagWrapper getcFW() { - return cookieFlagWrap; - } -} diff --git a/src/app/tokenposition/PostBody.java b/src/app/tokenposition/PostBody.java deleted file mode 100644 index 3e6aaca..0000000 --- a/src/app/tokenposition/PostBody.java +++ /dev/null @@ -1,99 +0,0 @@ -package app.tokenposition; - -import java.util.ArrayList; -import java.util.List; -import java.util.regex.Pattern; - -import org.apache.commons.lang.StringUtils; - -import app.helpers.Config; -import app.helpers.KeyValuePair; -import app.helpers.Output; -import app.helpers.TokenChecker; - -public class PostBody extends ITokenPosition { - - private String token; - private boolean found = false; - private String body; - - - public PostBody(List headersP, String body) { - this.body = body; - } - - @Override - public boolean positionFound() { - if (isRequest) { - KeyValuePair postJWT = getJWTFromPostBody(); - if (postJWT != null) { - found = true; - token = postJWT.getValue(); - return true; - } - } - return false; - } - - public KeyValuePair getJWTFromPostBody() { - int from = 0; - int index = body.contains("&") ? body.indexOf("&") : body.length(); - int parameterCount = StringUtils.countMatches(body, "&") + 1; - - List postParameterList = new ArrayList<>(); - for (int i = 0; i < parameterCount; i++) { - String parameter = body.substring(from, index); - parameter = parameter.replace("&", ""); - - String[] parameterSplit = parameter.split(Pattern.quote("=")); - if (parameterSplit.length > 1) { - String name = parameterSplit[0]; - String value = parameterSplit[1]; - postParameterList.add(new KeyValuePair(name, value)); - from = index; - index = body.indexOf("&", index + 1); - if (index == -1) { - index = body.length(); - } - } - } - for (String keyword : Config.tokenKeywords) { - for (KeyValuePair postParameter : postParameterList) { - if (keyword.equals(postParameter.getName()) && TokenChecker.isValidJWT(postParameter.getValue())) { - return postParameter; - } - } - } - return null; - } - - @Override - public String getToken() { - return found ? token : ""; - } - - @Override - public byte[] replaceToken(String newToken) { - body = replaceTokenImpl(newToken, body); - return getHelpers().buildHttpMessage(getHeaders(), body.getBytes()); - } - - public String replaceTokenImpl(String newToken, String body) { - boolean replaced = false; - // we cannot use the location of parameter, as the body might have changed, thus - // we need to search for it again - KeyValuePair postJWT = getJWTFromPostBody(); - for (String keyword : Config.tokenKeywords) { - if (keyword.equals(postJWT.getName())) { - String toReplace = postJWT.getNameAsParam() + postJWT.getValue(); - body = body.replace(toReplace, postJWT.getNameAsParam() + newToken); - replaced = true; - } - } - if (!replaced) { - Output.outputError("Could not replace token in post body."); - } - return body; - } - -} diff --git a/src/burp/BurpExtender.java b/src/burp/BurpExtender.java deleted file mode 100644 index f700f5c..0000000 --- a/src/burp/BurpExtender.java +++ /dev/null @@ -1,70 +0,0 @@ -package burp; - -import app.controllers.ContextMenuController; -import app.controllers.HighLightController; -import app.controllers.JWTInterceptTabController; -import app.controllers.JWTSuiteTabController; -import app.controllers.JWTTabController; -import app.helpers.Config; -import app.helpers.Output; -import gui.JWTInterceptTab; -import gui.JWTSuiteTab; -import gui.JWTViewTab; -import gui.RSyntaxTextAreaFactory; -import model.JWTInterceptModel; -import model.JWTSuiteTabModel; -import model.JWTTabModel; -import model.Settings; - -public class BurpExtender implements IBurpExtender, IMessageEditorTabFactory { - - private IBurpExtenderCallbacks callbacks; - private RSyntaxTextAreaFactory rSyntaxTextAreaFactory; - - @Override - public void registerExtenderCallbacks(IBurpExtenderCallbacks callbacks) { - this.callbacks = callbacks; - - Output.initialise(callbacks.getStdout(), callbacks.getStderr()); - Output.output("JWT4B says hi!"); - rSyntaxTextAreaFactory = new RSyntaxTextAreaFactory(callbacks); - - - callbacks.setExtensionName(Settings.EXTENSION_NAME); - callbacks.registerMessageEditorTabFactory(this); - - Config.loadConfig(); - - final HighLightController marker = new HighLightController(callbacks); - callbacks.registerHttpListener(marker); - - // Suite Tab - JWTSuiteTabModel jwtSTM = new JWTSuiteTabModel(); - JWTSuiteTab jwtST = new JWTSuiteTab(jwtSTM, rSyntaxTextAreaFactory); - JWTSuiteTabController jstC = new JWTSuiteTabController(jwtSTM, jwtST); - callbacks.addSuiteTab(jstC); - - // Context Menu - ContextMenuController cmC = new ContextMenuController(jstC); - callbacks.registerContextMenuFactory(cmC); - } - - @Override - public IMessageEditorTab createNewInstance(IMessageEditorController controller, boolean editable) { - IMessageEditorTab jwtTC; - if (editable) { // Intercept - JWTInterceptModel jwtSTM = new JWTInterceptModel(); - JWTInterceptTab jwtST = new JWTInterceptTab(jwtSTM, rSyntaxTextAreaFactory); - jwtTC = new JWTInterceptTabController(callbacks, jwtSTM, jwtST); - } else { - JWTTabModel jwtTM = new JWTTabModel(); - JWTViewTab jwtVT = new JWTViewTab(jwtTM, rSyntaxTextAreaFactory); - jwtTC = new JWTTabController(callbacks, jwtTM, jwtVT); - } - return jwtTC; - } - - public IBurpExtenderCallbacks getCallbacks() { - return callbacks; - } -} diff --git a/src/burp/api/montoya/BurpExtension.java b/src/burp/api/montoya/BurpExtension.java new file mode 100644 index 0000000..c22a314 --- /dev/null +++ b/src/burp/api/montoya/BurpExtension.java @@ -0,0 +1,24 @@ +/* + * Copyright (c) 2022-2023. PortSwigger Ltd. All rights reserved. + * + * This code may be used to extend the functionality of Burp Suite Community Edition + * and Burp Suite Professional, provided that this usage does not violate the + * license terms for those products. + */ + +package burp.api.montoya; + +/** + * All extensions must implement this interface. + *

+ * Implementations must be declared public, and must provide a default (public, no-argument) constructor. + */ +public interface BurpExtension +{ + /** + * Invoked when the extension is loaded. Any registered handlers will only be enabled once this method has completed. + * + * @param api The API implementation to access the functionality of Burp Suite. + */ + void initialize(MontoyaApi api); +} \ No newline at end of file diff --git a/src/burp/api/montoya/MontoyaApi.java b/src/burp/api/montoya/MontoyaApi.java new file mode 100644 index 0000000..23190ad --- /dev/null +++ b/src/burp/api/montoya/MontoyaApi.java @@ -0,0 +1,165 @@ +/* + * Copyright (c) 2022-2023. PortSwigger Ltd. All rights reserved. + * + * This code may be used to extend the functionality of Burp Suite Community Edition + * and Burp Suite Professional, provided that this usage does not violate the + * license terms for those products. + */ + +package burp.api.montoya; + +import burp.api.montoya.burpsuite.BurpSuite; +import burp.api.montoya.collaborator.Collaborator; +import burp.api.montoya.comparer.Comparer; +import burp.api.montoya.decoder.Decoder; +import burp.api.montoya.extension.Extension; +import burp.api.montoya.http.Http; +import burp.api.montoya.intruder.Intruder; +import burp.api.montoya.logging.Logging; +import burp.api.montoya.organizer.Organizer; +import burp.api.montoya.persistence.Persistence; +import burp.api.montoya.proxy.Proxy; +import burp.api.montoya.repeater.Repeater; +import burp.api.montoya.scanner.Scanner; +import burp.api.montoya.scope.Scope; +import burp.api.montoya.sitemap.SiteMap; +import burp.api.montoya.ui.UserInterface; +import burp.api.montoya.utilities.Utilities; +import burp.api.montoya.websocket.WebSockets; + +/** + * This interface is used by Burp Suite to pass a set of methods to extensions that can be used + * to perform various actions within Burp. When an extension is loaded, Burp invokes its + * {@link BurpExtension#initialize(MontoyaApi)} method and passes an instance + * of the {@link MontoyaApi} interface. The extension may then invoke the + * methods of this interface as required in order to extend Burp's + * functionality. + */ +public interface MontoyaApi +{ + /** + * Access functionality related to the Burp Suite application. + * + * @return An implementation of the BurpSuite interface which exposes application-level functionality. + */ + BurpSuite burpSuite(); + + /** + * [Professional only] Access the functionality of the Collaborator. + * + * @return An implementation of the Collaborator interface which exposes Collaborator functionality. + */ + Collaborator collaborator(); + + /** + * Access the functionality of the Comparer. + * + * @return An implementation of the Comparer interface which exposes Comparer functionality. + */ + Comparer comparer(); + + /** + * Access the functionality of the Decoder. + * + * @return An implementation of the Decoder interface which exposes Decoder functionality. + */ + Decoder decoder(); + + /** + * Access functionality related to your extension. + * + * @return An implementation of the Extension interface which exposes extension functionality. + */ + Extension extension(); + + /** + * Access the functionality related to HTTP requests and responses. + * + * @return An implementation of the Http interface which exposes http functionality. + */ + Http http(); + + /** + * Access the functionality of the Intruder. + * + * @return An implementation of the Comparer interface which exposes Comparer functionality. + */ + Intruder intruder(); + + /** + * Access the functionality related to logging and events. + * + * @return An implementation of the Logging interface which exposes logging functionality. + */ + Logging logging(); + + /** + * Access the functionality of the Organizer. + * + * @return An implementation of the Organizer interface which exposes Organizer functionality. + */ + Organizer organizer(); + + /** + * Access the functionality related to persistence. + * + * @return An implementation of the Persistence interface which exposes persistence functionality. + */ + Persistence persistence(); + + /** + * Access the functionality of the Proxy. + * + * @return An implementation of the Proxy interface which exposes Proxy functionality. + */ + Proxy proxy(); + + /** + * Access the functionality of the Repeater. + * + * @return An implementation of the Repeater interface which exposes Repeater functionality. + */ + Repeater repeater(); + + /** + * [Professional only] Access the functionality of the Scanner. + * + * @return An implementation of the Scanner interface which exposes Scanner functionality. + */ + Scanner scanner(); + + /** + * Access the functionality related to Burp's suite-wide target scope. + * + * @return An implementation of the Scope interface which exposes scope functionality. + */ + Scope scope(); + + /** + * Access the functionality of the Site Map. + * + * @return An implementation of the SiteMap interface which exposes sitemap functionality. + */ + SiteMap siteMap(); + + /** + * Access the functionality related to the user interface. + * + * @return An implementation of the UserInterface interface which exposes user interface functionality. + */ + UserInterface userInterface(); + + /** + * Access additional utilities. + * + * @return An implementation of the Utilities interface which exposes additional utilities. + */ + Utilities utilities(); + + /** + * Access the functionality related to WebSockets and messages. + * + * @return An implementation of the WebSockets interface which exposes WebSocket functionality. + */ + WebSockets websockets(); +} diff --git a/src/burp/api/montoya/burpsuite/BurpSuite.java b/src/burp/api/montoya/burpsuite/BurpSuite.java new file mode 100644 index 0000000..5aa4901 --- /dev/null +++ b/src/burp/api/montoya/burpsuite/BurpSuite.java @@ -0,0 +1,109 @@ +/* + * Copyright (c) 2022-2023. PortSwigger Ltd. All rights reserved. + * + * This code may be used to extend the functionality of Burp Suite Community Edition + * and Burp Suite Professional, provided that this usage does not violate the + * license terms for those products. + */ + +package burp.api.montoya.burpsuite; + +import burp.api.montoya.core.Version; + +import java.util.List; + +/** + * Provides access to functionality related to the Burp Suite application. + */ +public interface BurpSuite +{ + /** + * Retrieve information about the version of Burp in which the + * extension is running. It can be used by extensions to dynamically adjust + * their behavior depending on the functionality and APIs supported by the + * current version. + * + * @return The Burp {@link Version}. + */ + Version version(); + + /** + * Export current project-level configuration in JSON format. + * This is the same format that can be saved and loaded via + * the Burp user interface. To include only certain sections of the + * configuration, you can optionally supply the path to each section that + * should be included, for example: "project_options.connections". If no + * paths are provided, then the entire configuration will be saved. + * + * @param paths A list of Strings representing the path to each + * configuration section that should be included. + * + * @return A String representing the current configuration in JSON format. + */ + String exportProjectOptionsAsJson(String... paths); + + /** + * Import a new project-level configuration from the JSON String provided. + * This is the same format that can be saved and + * loaded via the Burp user interface. Partial configurations are + * acceptable, and any settings not specified will be left unmodified. + *

+ * Any user-level configuration options contained in the input will be + * ignored. + * + * @param json A JSON String containing the new configuration. + */ + void importProjectOptionsFromJson(String json); + + /** + * Export current user-level configuration in JSON format. + * This is the same format that can be saved and loaded via + * the Burp user interface. To include only certain sections of the + * configuration, you can optionally supply the path to each section that + * should be included, for example: "user_options.connections". If no + * paths are provided, then the entire configuration will be saved. + * + * @param paths A list of Strings representing the path to each + * configuration section that should be included. + * + * @return A String representing the current configuration in JSON format. + */ + String exportUserOptionsAsJson(String... paths); + + /** + * Import a new user-level configuration from the JSON String provided. + * This is the same format that can be saved and + * loaded via the Burp user interface. Partial configurations are + * acceptable, and any settings not specified will be left unmodified. + *

+ * Any project-level configuration options contained in the input will be + * ignored. + * + * @param json A JSON String containing the new configuration. + */ + void importUserOptionsFromJson(String json); + + /** + * Command line arguments that were passed to Burp on startup. + * + * @return The command line arguments that were passed to Burp on startup. + */ + List commandLineArguments(); + + /** + * Shut down Burp programmatically. + * + * @param options The shutdown options for shutting down Burp + * programmatically. For example {@link ShutdownOptions#PROMPT_USER} will + * display a dialog to the user allowing them to confirm or cancel the + * shutdown. + */ + void shutdown(ShutdownOptions... options); + + /** + * Access the functionality of the task execution engine. + * + * @return An implementation of the TaskExecutionEngine interface which exposes task execution engine functionality. + */ + TaskExecutionEngine taskExecutionEngine(); +} diff --git a/src/burp/api/montoya/burpsuite/ShutdownOptions.java b/src/burp/api/montoya/burpsuite/ShutdownOptions.java new file mode 100644 index 0000000..e3011b9 --- /dev/null +++ b/src/burp/api/montoya/burpsuite/ShutdownOptions.java @@ -0,0 +1,20 @@ +/* + * Copyright (c) 2022-2023. PortSwigger Ltd. All rights reserved. + * + * This code may be used to extend the functionality of Burp Suite Community Edition + * and Burp Suite Professional, provided that this usage does not violate the + * license terms for those products. + */ + +package burp.api.montoya.burpsuite; + +/** + * Shutdown options that can be used when calling {@link BurpSuite#shutdown(ShutdownOptions...)}. + */ +public enum ShutdownOptions +{ + /** + * Display a dialog to the user allowing them to confirm or cancel the shutdown + */ + PROMPT_USER +} diff --git a/src/burp/api/montoya/burpsuite/TaskExecutionEngine.java b/src/burp/api/montoya/burpsuite/TaskExecutionEngine.java new file mode 100644 index 0000000..086855e --- /dev/null +++ b/src/burp/api/montoya/burpsuite/TaskExecutionEngine.java @@ -0,0 +1,37 @@ +/* + * Copyright (c) 2022-2023. PortSwigger Ltd. All rights reserved. + * + * This code may be used to extend the functionality of Burp Suite Community Edition + * and Burp Suite Professional, provided that this usage does not violate the + * license terms for those products. + */ + +package burp.api.montoya.burpsuite; + +/** + * Provides access to the task execution engine. + */ +public interface TaskExecutionEngine +{ + /** + * Task execution engine state + */ + enum TaskExecutionEngineState + { + RUNNING, PAUSED + } + + /** + * Retrieves the current state of the task execution engine. + * + * @return current state + */ + TaskExecutionEngineState getState(); + + /** + * Sets the task execution engine state + * + * @param state new state + */ + void setState(TaskExecutionEngineState state); +} diff --git a/src/burp/api/montoya/collaborator/Collaborator.java b/src/burp/api/montoya/collaborator/Collaborator.java new file mode 100644 index 0000000..5cdbe90 --- /dev/null +++ b/src/burp/api/montoya/collaborator/Collaborator.java @@ -0,0 +1,47 @@ +/* + * Copyright (c) 2022-2023. PortSwigger Ltd. All rights reserved. + * + * This code may be used to extend the functionality of Burp Suite Community Edition + * and Burp Suite Professional, provided that this usage does not violate the + * license terms for those products. + */ + +package burp.api.montoya.collaborator; + +/** + * [Professional only] Provides access to the facilities of Burp Collaborator. + */ +public interface Collaborator +{ + /** + * Create a new Burp Collaborator client + * that can be used to generate Burp Collaborator payloads and poll the + * Collaborator server for any network interactions that result from using + * those payloads. + * + * @return A new instance of {@link CollaboratorClient} that can be used to + * generate Collaborator payloads and retrieve interactions. + */ + CollaboratorClient createClient(); + + /** + * Restore a {@link CollaboratorClient} from a previous + * session. This allows you to retrieve the interactions that were identified + * from a specific payloads. + * + * @param secretKey The key to restore the {@link CollaboratorClient} from the previous session. + * + * @return A new instance of {@link CollaboratorClient} that can be used to + * generate Collaborator payloads and retrieve interactions. + */ + CollaboratorClient restoreClient(SecretKey secretKey); + + /** + * Obtain Burp's default Collaborator payload generator. + * This enables you to generate Collaborator payloads that are linked to the Collaborator tab. + * Any interactions are shown in the Collaborator results tab that was open when the payload was generated. + * + * @return The current instance of Burp's default {@link CollaboratorPayloadGenerator}. + */ + CollaboratorPayloadGenerator defaultPayloadGenerator(); +} diff --git a/src/burp/api/montoya/collaborator/CollaboratorClient.java b/src/burp/api/montoya/collaborator/CollaboratorClient.java new file mode 100644 index 0000000..85adbf3 --- /dev/null +++ b/src/burp/api/montoya/collaborator/CollaboratorClient.java @@ -0,0 +1,98 @@ +/* + * Copyright (c) 2022-2023. PortSwigger Ltd. All rights reserved. + * + * This code may be used to extend the functionality of Burp Suite Community Edition + * and Burp Suite Professional, provided that this usage does not violate the + * license terms for those products. + */ + +package burp.api.montoya.collaborator; + +import java.util.List; + +/** + * Burp Collaborator client + * that can be used to generate Burp Collaborator payloads and poll the + * Collaborator server for any network interactions that result from using + * those payloads. Extensions can obtain new instances of this class by + * calling {@link Collaborator#createClient()}. + *

+ * Note that each Burp Collaborator client is tied to the Collaborator + * server configuration that was in place at the time the client was created. + *

+ */ +public interface CollaboratorClient extends CollaboratorPayloadGenerator +{ + /** + * Generate new Burp Collaborator payloads. Options + * can be specified to alter the way the payloads are generated. If no + * options are specified, generated payloads will include the server + * location. + * + * @param options The optional payload options to apply + * + * @return The generated payload. + * + * @throws IllegalStateException if Burp Collaborator is disabled + */ + @Override + CollaboratorPayload generatePayload(PayloadOption... options); + + /** + * Generate new Burp Collaborator payloads with custom data. + * The custom data can be retrieved from any {@link Interaction} triggered. + * Options can be specified to alter the way the payloads are generated. If no + * options are specified, generated payloads will include the server location. + * + * @param customData The custom data to add to the payload. Maximum size is 16 characters. Must be alphanumeric. + * @param options The optional payload options to apply + * + * @return The generated payload. + * + * @throws IllegalStateException if Burp Collaborator is disabled + */ + CollaboratorPayload generatePayload(String customData, PayloadOption... options); + + /** + * Retrieve all Collaborator server interactions + * resulting from payloads that were generated for this client. + * + * @return The Collaborator interactions that have occurred resulting from + * payloads that were generated for this client. + * + * @throws IllegalStateException if Burp Collaborator is disabled + */ + List getAllInteractions(); + + /** + * Retrieve filtered Collaborator server + * interactions resulting from payloads that were generated for this + * client. Only interactions matching the supplied filter will be returned. + * + * @param filter The filter that will be applied to each interaction. + * + * @return The filtered Collaborator interactions resulting from payloads + * that were generated for this client. + * + * @throws IllegalStateException if Burp Collaborator is disabled + */ + List getInteractions(InteractionFilter filter); + + /** + * Retrieve the details of the Collaborator server + * associated with this client. + * + * @return The Collaborator server details. + * + * @throws IllegalStateException if Burp Collaborator is disabled + */ + CollaboratorServer server(); + + /** + * Secret key that is associated with this client context. + * The key can be used to re-create this client again with the interaction data if required. + * + * @return The {@link SecretKey} that is associated with this Collaborator client. + */ + SecretKey getSecretKey(); +} diff --git a/src/burp/api/montoya/collaborator/CollaboratorPayload.java b/src/burp/api/montoya/collaborator/CollaboratorPayload.java new file mode 100644 index 0000000..1e48c9b --- /dev/null +++ b/src/burp/api/montoya/collaborator/CollaboratorPayload.java @@ -0,0 +1,49 @@ +/* + * Copyright (c) 2022-2023. PortSwigger Ltd. All rights reserved. + * + * This code may be used to extend the functionality of Burp Suite Community Edition + * and Burp Suite Professional, provided that this usage does not violate the + * license terms for those products. + */ + +package burp.api.montoya.collaborator; + +import java.util.Optional; + +/** + * Burp Collaborator payload. + */ +public interface CollaboratorPayload +{ + /** + * Payload's interaction id. + * + * @return The interaction id of the payload. + */ + InteractionId id(); + + /** + * Custom data from the payload. + * + * @return The payload's custom data. + */ + Optional customData(); + + /** + * Optional instance of CollaboratorServer describing the + * server location for this payload. If the payload was generated without + * the server location this method will return an empty Optional. + * + * @return Details of the collaborator server referenced in the payload + * or empty if the payload was generated without the server location. + */ + Optional server(); + + /** + * The payload. + * + * @return The payload string. + */ + @Override + String toString(); +} diff --git a/src/burp/api/montoya/collaborator/CollaboratorPayloadGenerator.java b/src/burp/api/montoya/collaborator/CollaboratorPayloadGenerator.java new file mode 100644 index 0000000..01d0045 --- /dev/null +++ b/src/burp/api/montoya/collaborator/CollaboratorPayloadGenerator.java @@ -0,0 +1,30 @@ +/* + * Copyright (c) 2022-2023. PortSwigger Ltd. All rights reserved. + * + * This code may be used to extend the functionality of Burp Suite Community Edition + * and Burp Suite Professional, provided that this usage does not violate the + * license terms for those products. + */ + +package burp.api.montoya.collaborator; + +/** + * Burp Collaborator payload generator + * that can be used to generate Burp Collaborator payloads. + */ +public interface CollaboratorPayloadGenerator +{ + /** + * Generate new Burp Collaborator payloads. Options + * can be specified to alter the way the payloads are generated. If no + * options are specified, generated payloads will include the server + * location. + * + * @param options The optional payload options to apply + * + * @return The generated payload. + * + * @throws IllegalStateException if Burp Collaborator is disabled + */ + CollaboratorPayload generatePayload(PayloadOption... options); +} diff --git a/src/burp/api/montoya/collaborator/CollaboratorServer.java b/src/burp/api/montoya/collaborator/CollaboratorServer.java new file mode 100644 index 0000000..93d7410 --- /dev/null +++ b/src/burp/api/montoya/collaborator/CollaboratorServer.java @@ -0,0 +1,31 @@ +/* + * Copyright (c) 2022-2023. PortSwigger Ltd. All rights reserved. + * + * This code may be used to extend the functionality of Burp Suite Community Edition + * and Burp Suite Professional, provided that this usage does not violate the + * license terms for those products. + */ + +package burp.api.montoya.collaborator; + +/** + * Provides details of the Collaborator server associated with + * this client. + */ +public interface CollaboratorServer +{ + /** + * Address of the Collaborator server. + * + * @return The hostname or IP address of the Collaborator server. + */ + String address(); + + /** + * Indicates whether the server address is an IP address. + * + * @return {@code true} if the address is an IP address; {@code false} + * otherwise. + */ + boolean isLiteralAddress(); +} diff --git a/src/burp/api/montoya/collaborator/DnsDetails.java b/src/burp/api/montoya/collaborator/DnsDetails.java new file mode 100644 index 0000000..245b4ad --- /dev/null +++ b/src/burp/api/montoya/collaborator/DnsDetails.java @@ -0,0 +1,32 @@ +/* + * Copyright (c) 2022-2023. PortSwigger Ltd. All rights reserved. + * + * This code may be used to extend the functionality of Burp Suite Community Edition + * and Burp Suite Professional, provided that this usage does not violate the + * license terms for those products. + */ + +package burp.api.montoya.collaborator; + +import burp.api.montoya.core.ByteArray; + +/** + * Provides information about a DNS interaction detected by Burp + * Collaborator. + */ +public interface DnsDetails +{ + /** + * DNS query type. + * + * @return The type of DNS query performed by the interaction. + */ + DnsQueryType queryType(); + + /** + * Raw DNS query. + * + * @return The raw DNS query sent to the Collaborator server. + */ + ByteArray query(); +} diff --git a/src/burp/api/montoya/collaborator/DnsQueryType.java b/src/burp/api/montoya/collaborator/DnsQueryType.java new file mode 100644 index 0000000..1c4870b --- /dev/null +++ b/src/burp/api/montoya/collaborator/DnsQueryType.java @@ -0,0 +1,85 @@ +/* + * Copyright (c) 2022-2023. PortSwigger Ltd. All rights reserved. + * + * This code may be used to extend the functionality of Burp Suite Community Edition + * and Burp Suite Professional, provided that this usage does not violate the + * license terms for those products. + */ + +package burp.api.montoya.collaborator; + +/** + * Domain Name System (DNS) query types. + */ +public enum DnsQueryType +{ + /** + * Address Record + */ + A, + /** + * IPv6 address record + */ + AAAA, + /** + * All cached records + */ + ALL, + /** + * Certification Authority Authorization + */ + CAA, + /** + * Canonical name record + */ + CNAME, + /** + * DNS Key record + */ + DNSKEY, + /** + * Delegation signer + */ + DS, + /** + * Host Information + */ + HINFO, + /** + * HTTPS Binding + */ + HTTPS, + /** + * Mail exchange record + */ + MX, + /** + * Naming Authority Pointer + */ + NAPTR, + /** + * Name Server Record + */ + NS, + /** + * PTR Resource Record + */ + PTR, + /** + * Start of authority record + */ + SOA, + /** + * Service locator + */ + SRV, + /** + * Text record + */ + TXT, + /** + * Unknown / Not Mapped / Obsolete + */ + UNKNOWN + +} diff --git a/src/burp/api/montoya/collaborator/HttpDetails.java b/src/burp/api/montoya/collaborator/HttpDetails.java new file mode 100644 index 0000000..004fe32 --- /dev/null +++ b/src/burp/api/montoya/collaborator/HttpDetails.java @@ -0,0 +1,34 @@ +/* + * Copyright (c) 2022-2023. PortSwigger Ltd. All rights reserved. + * + * This code may be used to extend the functionality of Burp Suite Community Edition + * and Burp Suite Professional, provided that this usage does not violate the + * license terms for those products. + */ + +package burp.api.montoya.collaborator; + +import burp.api.montoya.http.HttpProtocol; +import burp.api.montoya.http.message.HttpRequestResponse; + +/** + * Provides information about an HTTP interaction detected by + * Burp Collaborator. + */ +public interface HttpDetails +{ + /** + * HTTP protocol. + * + * @return The HTTP protocol used by the interaction. + */ + HttpProtocol protocol(); + + /** + * HTTP request and response. + * + * @return The HTTP request sent to the Collaborator server and the + * server's response. + */ + HttpRequestResponse requestResponse(); +} diff --git a/src/burp/api/montoya/collaborator/Interaction.java b/src/burp/api/montoya/collaborator/Interaction.java new file mode 100644 index 0000000..130d022 --- /dev/null +++ b/src/burp/api/montoya/collaborator/Interaction.java @@ -0,0 +1,86 @@ +/* + * Copyright (c) 2022-2023. PortSwigger Ltd. All rights reserved. + * + * This code may be used to extend the functionality of Burp Suite Community Edition + * and Burp Suite Professional, provided that this usage does not violate the + * license terms for those products. + */ + +package burp.api.montoya.collaborator; + +import java.net.InetAddress; +import java.time.ZonedDateTime; +import java.util.Optional; + +/** + * Provides details of an interaction with the Burp Collaborator + * server. + */ +public interface Interaction +{ + /** + * Interaction id. + * + * @return The interaction id. + */ + InteractionId id(); + + /** + * Interaction Type. + * + * @return The type of interaction. + */ + InteractionType type(); + + /** + * Timestamp of the interaction. + * + * @return The timestamp of the interaction. + */ + ZonedDateTime timeStamp(); + + /** + * Client IP address of the interaction. + * + * @return The IP address of the client performing the interaction. + */ + InetAddress clientIp(); + + /** + * Client port of the interaction. + * + * @return The port of the client initiating the interaction. + */ + int clientPort(); + + /** + * DNS interaction details. + * + * @return Details of the DNS interaction or empty if the interaction was + * not DNS. + */ + Optional dnsDetails(); + + /** + * HTTP interaction details. + * + * @return Details of the HTTP interaction or empty if the interaction was + * not HTTP. + */ + Optional httpDetails(); + + /** + * SMTP interaction details. + * + * @return Details of the SMTP interaction or empty if the interaction was + * not SMTP. + */ + Optional smtpDetails(); + + /** + * Custom data from the payload. + * + * @return The custom data. + */ + Optional customData(); +} diff --git a/src/burp/api/montoya/collaborator/InteractionFilter.java b/src/burp/api/montoya/collaborator/InteractionFilter.java new file mode 100644 index 0000000..793b431 --- /dev/null +++ b/src/burp/api/montoya/collaborator/InteractionFilter.java @@ -0,0 +1,61 @@ +/* + * Copyright (c) 2022-2023. PortSwigger Ltd. All rights reserved. + * + * This code may be used to extend the functionality of Burp Suite Community Edition + * and Burp Suite Professional, provided that this usage does not violate the + * license terms for those products. + */ + +package burp.api.montoya.collaborator; + +import static burp.api.montoya.internal.ObjectFactoryLocator.FACTORY; + +/** + * Provides a filtering mechanism for use when retrieving + * interactions from the Burp Collaborator server. + * Helper methods are provided to create filters based on the interaction id + * and the payload. + */ +public interface InteractionFilter +{ + /** + * This method is invoked for each interaction retrieved from the + * Collaborator server and determines whether the interaction should be + * included in the list of interactions returned. + * + * @param server The collaborator server that received the interaction. + * @param interaction The interaction details. + * + * @return {@code true} if the interaction should be included, + * {@code false} if not. + */ + boolean matches(CollaboratorServer server, Interaction interaction); + + /** + * Construct a InteractionFilter that matches any + * interaction with the specified interaction id. + * + * @param id The interaction id. + * + * @return {@code true} if the interaction has the specified id, + * {@code false} if not. + */ + static InteractionFilter interactionIdFilter(String id) + { + return FACTORY.interactionIdFilter(id); + } + + /** + * Construct an InteractionFilter that matches any + * interaction with the specified payload. + * + * @param payload The payload. + * + * @return {@code true} if the interaction has the specified payload, + * {@code false} if not. + */ + static InteractionFilter interactionPayloadFilter(String payload) + { + return FACTORY.interactionPayloadFilter(payload); + } +} diff --git a/src/burp/api/montoya/collaborator/InteractionId.java b/src/burp/api/montoya/collaborator/InteractionId.java new file mode 100644 index 0000000..8ce42ad --- /dev/null +++ b/src/burp/api/montoya/collaborator/InteractionId.java @@ -0,0 +1,24 @@ +/* + * Copyright (c) 2022-2023. PortSwigger Ltd. All rights reserved. + * + * This code may be used to extend the functionality of Burp Suite Community Edition + * and Burp Suite Professional, provided that this usage does not violate the + * license terms for those products. + */ + +package burp.api.montoya.collaborator; + + +/** + * Burp Collaborator interaction id. + */ +public interface InteractionId +{ + /** + * Interaction id. + * + * @return The interaction id string. + */ + @Override + String toString(); +} diff --git a/src/burp/api/montoya/collaborator/InteractionType.java b/src/burp/api/montoya/collaborator/InteractionType.java new file mode 100644 index 0000000..5d7352f --- /dev/null +++ b/src/burp/api/montoya/collaborator/InteractionType.java @@ -0,0 +1,28 @@ +/* + * Copyright (c) 2022-2023. PortSwigger Ltd. All rights reserved. + * + * This code may be used to extend the functionality of Burp Suite Community Edition + * and Burp Suite Professional, provided that this usage does not violate the + * license terms for those products. + */ + +package burp.api.montoya.collaborator; + +/** + * Possible types of interaction with Burp Collaborator. + */ +public enum InteractionType +{ + /** + * Domain Name System + */ + DNS, + /** + * Hypertext Transfer Protocol + */ + HTTP, + /** + * Simple Mail Transfer Protocol + */ + SMTP +} diff --git a/src/burp/api/montoya/collaborator/PayloadOption.java b/src/burp/api/montoya/collaborator/PayloadOption.java new file mode 100644 index 0000000..6dba718 --- /dev/null +++ b/src/burp/api/montoya/collaborator/PayloadOption.java @@ -0,0 +1,20 @@ +/* + * Copyright (c) 2023. PortSwigger Ltd. All rights reserved. + * + * This code may be used to extend the functionality of Burp Suite Community Edition + * and Burp Suite Professional, provided that this usage does not violate the + * license terms for those products. + */ + +package burp.api.montoya.collaborator; + +/** + * Options that can be specified when generating Burp Collaborator payloads. + */ +public enum PayloadOption +{ + /** + * Generate a payload excluding the server location + */ + WITHOUT_SERVER_LOCATION +} diff --git a/src/burp/api/montoya/collaborator/SecretKey.java b/src/burp/api/montoya/collaborator/SecretKey.java new file mode 100644 index 0000000..f3f634a --- /dev/null +++ b/src/burp/api/montoya/collaborator/SecretKey.java @@ -0,0 +1,39 @@ +/* + * Copyright (c) 2023. PortSwigger Ltd. All rights reserved. + * + * This code may be used to extend the functionality of Burp Suite Community Edition + * and Burp Suite Professional, provided that this usage does not violate the + * license terms for those products. + */ + +package burp.api.montoya.collaborator; + +import static burp.api.montoya.internal.ObjectFactoryLocator.FACTORY; + +/** + * Secret key that is associated with a {@link CollaboratorClient} + */ +public interface SecretKey +{ + /** + * Secret key in string form. + * + * @return The base64 encoded secret key. + */ + @Override + String toString(); + + /** + * Create an instance of {@link SecretKey} which + * you will be able to use to restore a previously created {@link CollaboratorClient} + * with the {@link Collaborator#restoreClient(SecretKey)} method. + * + * @param encodedKey The base64 encoded raw secret key. + * + * @return An instance of {@link SecretKey} wrapping the provided secret key. + */ + static SecretKey secretKey(String encodedKey) + { + return FACTORY.secretKey(encodedKey); + } +} diff --git a/src/burp/api/montoya/collaborator/SmtpDetails.java b/src/burp/api/montoya/collaborator/SmtpDetails.java new file mode 100644 index 0000000..6826f76 --- /dev/null +++ b/src/burp/api/montoya/collaborator/SmtpDetails.java @@ -0,0 +1,30 @@ +/* + * Copyright (c) 2022-2023. PortSwigger Ltd. All rights reserved. + * + * This code may be used to extend the functionality of Burp Suite Community Edition + * and Burp Suite Professional, provided that this usage does not violate the + * license terms for those products. + */ + +package burp.api.montoya.collaborator; + +/** + * SMTP interaction detected by Burp Collaborator. + */ +public interface SmtpDetails +{ + /** + * SMTP protocol. + * + * @return The protocol used by the interaction. + */ + SmtpProtocol protocol(); + + /** + * SMTP conversation. + * + * @return The SMTP conversation between the client and the Collaborator + * server. + */ + String conversation(); +} diff --git a/src/burp/api/montoya/collaborator/SmtpProtocol.java b/src/burp/api/montoya/collaborator/SmtpProtocol.java new file mode 100644 index 0000000..c498b1e --- /dev/null +++ b/src/burp/api/montoya/collaborator/SmtpProtocol.java @@ -0,0 +1,24 @@ +/* + * Copyright (c) 2022-2023. PortSwigger Ltd. All rights reserved. + * + * This code may be used to extend the functionality of Burp Suite Community Edition + * and Burp Suite Professional, provided that this usage does not violate the + * license terms for those products. + */ + +package burp.api.montoya.collaborator; + +/** + * Simple Mail Transfer Protocol (SMTP) protocols. + */ +public enum SmtpProtocol +{ + /** + * Simple Mail Transfer Protocol + */ + SMTP, + /** + * Simple Mail Transfer Protocol Secure + */ + SMTPS +} diff --git a/src/burp/api/montoya/comparer/Comparer.java b/src/burp/api/montoya/comparer/Comparer.java new file mode 100644 index 0000000..94bf555 --- /dev/null +++ b/src/burp/api/montoya/comparer/Comparer.java @@ -0,0 +1,24 @@ +/* + * Copyright (c) 2022-2023. PortSwigger Ltd. All rights reserved. + * + * This code may be used to extend the functionality of Burp Suite Community Edition + * and Burp Suite Professional, provided that this usage does not violate the + * license terms for those products. + */ + +package burp.api.montoya.comparer; + +import burp.api.montoya.core.ByteArray; + +/** + * Provides access to the functionality of the Comparer tool. + */ +public interface Comparer +{ + /** + * Send data to the Comparer tool. + * + * @param data The data to be sent to Comparer. + */ + void sendToComparer(ByteArray... data); +} diff --git a/src/burp/api/montoya/core/Annotations.java b/src/burp/api/montoya/core/Annotations.java new file mode 100644 index 0000000..5fc447f --- /dev/null +++ b/src/burp/api/montoya/core/Annotations.java @@ -0,0 +1,116 @@ +/* + * Copyright (c) 2022-2023. PortSwigger Ltd. All rights reserved. + * + * This code may be used to extend the functionality of Burp Suite Community Edition + * and Burp Suite Professional, provided that this usage does not violate the + * license terms for those products. + */ + +package burp.api.montoya.core; + +import static burp.api.montoya.internal.ObjectFactoryLocator.FACTORY; + +/** + * Annotations stored with requests and responses in Burp Suite. + */ +public interface Annotations +{ + /** + * @return the notes + */ + String notes(); + + /** + * @return True if there are any notes for this HTTP request and response. + */ + boolean hasNotes(); + + /** + * @return True if there is a highlight color for this HTTP request and response. + */ + boolean hasHighlightColor(); + + /** + * Set (mutate) the current annotations notes value + * + * @param notes the notes to set on the current annotations + */ + void setNotes(String notes); + + /** + * @return the highlight color; + */ + HighlightColor highlightColor(); + + /** + * Set (mutate) the current annotations highlight color value + * + * @param highlightColor the highlight color to set on the current annotations + */ + void setHighlightColor(HighlightColor highlightColor); + + /** + * Create a copy of the annotations with new notes. + * + * @param notes The new notes. + * + * @return The new annotations. + */ + Annotations withNotes(String notes); + + /** + * Create a copy of the annotations with a new highlight color. + * + * @param highlightColor The new highlight color. + * + * @return The new annotations. + */ + Annotations withHighlightColor(HighlightColor highlightColor); + + /** + * Create a new empty annotations. + * + * @return The annotations. + */ + static Annotations annotations() + { + return FACTORY.annotations(); + } + + /** + * Create a new annotations with notes. + * + * @param notes The notes of the annotations + * + * @return The annotations. + */ + static Annotations annotations(String notes) + { + return FACTORY.annotations(notes); + } + + /** + * Create a new annotations with a highlight color. + * + * @param highlightColor The highlight color of the annotations + * + * @return The annotations. + */ + static Annotations annotations(HighlightColor highlightColor) + { + return FACTORY.annotations(highlightColor); + } + + /** + * Create a new annotations with notes and a highlight color. + * + * @param notes The notes of the annotations + * @param highlightColor The highlight color of the annotations + * + * @return The annotations. + */ + static Annotations annotations(String notes, HighlightColor highlightColor) + { + return FACTORY.annotations(notes, highlightColor); + } +} diff --git a/src/burp/api/montoya/core/BurpSuiteEdition.java b/src/burp/api/montoya/core/BurpSuiteEdition.java new file mode 100644 index 0000000..11f35ba --- /dev/null +++ b/src/burp/api/montoya/core/BurpSuiteEdition.java @@ -0,0 +1,43 @@ +/* + * Copyright (c) 2023. PortSwigger Ltd. All rights reserved. + * + * This code may be used to extend the functionality of Burp Suite Community Edition + * and Burp Suite Professional, provided that this usage does not violate the + * license terms for those products. + */ + +package burp.api.montoya.core; + +/** + * Editions of Burp Suite. + */ +public enum BurpSuiteEdition +{ + /** + * Burp Suite professional edition + */ + PROFESSIONAL("Professional"), + /** + * Burp Suite community edition + */ + COMMUNITY_EDITION("Community Edition"), + /** + * Burp Suite enterprise edition + */ + ENTERPRISE_EDITION("Enterprise Edition"); + + private final String displayName; + + BurpSuiteEdition(String displayName) + { + this.displayName = displayName; + } + + /** + * @return displayName for this edition of Burp Suite. + */ + public String displayName() + { + return displayName; + } +} diff --git a/src/burp/api/montoya/core/ByteArray.java b/src/burp/api/montoya/core/ByteArray.java new file mode 100644 index 0000000..3cfcb3a --- /dev/null +++ b/src/burp/api/montoya/core/ByteArray.java @@ -0,0 +1,374 @@ +/* + * Copyright (c) 2023. PortSwigger Ltd. All rights reserved. + * + * This code may be used to extend the functionality of Burp Suite Community Edition + * and Burp Suite Professional, provided that this usage does not violate the + * license terms for those products. + */ + +package burp.api.montoya.core; + + +import java.util.regex.Pattern; + +import static burp.api.montoya.internal.ObjectFactoryLocator.FACTORY; + +/** + * Burp ByteArray with various methods for querying and manipulating byte arrays. + */ +public interface ByteArray extends Iterable +{ + /** + * Access the byte stored at the provided index. + * + * @param index Index of the byte to be retrieved. + * + * @return The byte at the index. + */ + byte getByte(int index); + + /** + * Sets the byte at the provided index to the provided byte. + * + * @param index Index of the byte to be set. + * @param value The byte to be set. + */ + void setByte(int index, byte value); + + /** + * Sets the byte at the provided index to the provided narrowed integer value. + * + * @param index Index of the byte to be set. + * @param value The integer value to be set after a narrowing primitive conversion to a byte. + */ + void setByte(int index, int value); + + /** + * Sets bytes starting at the specified index to the provided bytes. + * + * @param index The index of the first byte to set. + * @param data The byte[] or sequence of bytes to be set. + */ + void setBytes(int index, byte... data); + + /** + * Sets bytes starting at the specified index to the provided integers after narrowing primitive conversion to bytes. + * + * @param index The index of the first byte to set. + * @param data The int[] or the sequence of integers to be set after a narrowing primitive conversion to bytes. + */ + void setBytes(int index, int... data); + + /** + * Sets bytes starting at the specified index to the provided bytes. + * + * @param index The index of the first byte to set. + * @param byteArray The {@code ByteArray} object holding the provided bytes. + */ + void setBytes(int index, ByteArray byteArray); + + /** + * Number of bytes stored in the {@code ByteArray}. + * + * @return Length of the {@code ByteArray}. + */ + int length(); + + /** + * Copy of all bytes + * + * @return Copy of all bytes. + */ + byte[] getBytes(); + + /** + * New ByteArray with all bytes between the start index (inclusive) and the end index (exclusive). + * + * @param startIndexInclusive The inclusive start index of retrieved range. + * @param endIndexExclusive The exclusive end index of retrieved range. + * + * @return ByteArray containing all bytes in the specified range. + */ + ByteArray subArray(int startIndexInclusive, int endIndexExclusive); + + /** + * New ByteArray with all bytes in the specified range. + * + * @param range The {@link Range} of bytes to be returned. + * + * @return ByteArray containing all bytes in the specified range. + */ + ByteArray subArray(Range range); + + /** + * Create a copy of the {@code ByteArray} + * + * @return New {@code ByteArray} with a copy of the wrapped bytes. + */ + ByteArray copy(); + + /** + * Create a copy of the {@code ByteArray} in temporary file.
+ * This method is used to save the {@code ByteArray} object to a temporary file, + * so that it is no longer held in memory. Extensions can use this method to convert + * {@code ByteArray} objects into a form suitable for long-term usage. + * + * @return A new {@code ByteArray} instance stored in temporary file. + */ + ByteArray copyToTempFile(); + + /** + * Searches the data in the ByteArray for the first occurrence of a specified term. + * It works on byte-based data in a way that is similar to the way the native Java method {@link String#indexOf(String)} works on String-based data. + * + * @param searchTerm The value to be searched for. + * + * @return The offset of the first occurrence of the pattern within the specified bounds, or -1 if no match is found. + */ + int indexOf(ByteArray searchTerm); + + /** + * Searches the data in the ByteArray for the first occurrence of a specified term. + * It works on byte-based data in a way that is similar to the way the native Java method {@link String#indexOf(String)} works on String-based data. + * + * @param searchTerm The value to be searched for. + * + * @return The offset of the first occurrence of the pattern within the specified bounds, or -1 if no match is found. + */ + int indexOf(String searchTerm); + + /** + * Searches the data in the ByteArray for the first occurrence of a specified term. + * It works on byte-based data in a way that is similar to the way the native Java method {@link String#indexOf(String)} works on String-based data. + * + * @param searchTerm The value to be searched for. + * @param caseSensitive Flags whether the search is case-sensitive. + * + * @return The offset of the first occurrence of the pattern within the specified bounds, or -1 if no match is found. + */ + int indexOf(ByteArray searchTerm, boolean caseSensitive); + + /** + * Searches the data in the ByteArray for the first occurrence of a specified term. + * It works on byte-based data in a way that is similar to the way the native Java method {@link String#indexOf(String)} works on String-based data. + * + * @param searchTerm The value to be searched for. + * @param caseSensitive Flags whether the search is case-sensitive. + * + * @return The offset of the first occurrence of the pattern within the specified bounds, or -1 if no match is found. + */ + int indexOf(String searchTerm, boolean caseSensitive); + + /** + * Searches the data in the ByteArray for the first occurrence of a specified term. + * It works on byte-based data in a way that is similar to the way the native Java method {@link String#indexOf(String)} works on String-based data. + * + * @param searchTerm The value to be searched for. + * @param caseSensitive Flags whether the search is case-sensitive. + * @param startIndexInclusive The inclusive start index for the search. + * @param endIndexExclusive The exclusive end index for the search. + * + * @return The offset of the first occurrence of the pattern within the specified bounds, or -1 if no match is found. + */ + int indexOf(ByteArray searchTerm, boolean caseSensitive, int startIndexInclusive, int endIndexExclusive); + + /** + * Searches the data in the ByteArray for the first occurrence of a specified term. + * It works on byte-based data in a way that is similar to the way the native Java method {@link String#indexOf(String)} works on String-based data. + * + * @param searchTerm The value to be searched for. + * @param caseSensitive Flags whether the search is case-sensitive. + * @param startIndexInclusive The inclusive start index for the search. + * @param endIndexExclusive The exclusive end index for the search. + * + * @return The offset of the first occurrence of the pattern within the specified bounds, or -1 if no match is found. + */ + int indexOf(String searchTerm, boolean caseSensitive, int startIndexInclusive, int endIndexExclusive); + + /** + * Searches the data in the ByteArray for the first occurrence of a specified pattern. + * + * @param pattern The pattern to be matched. + * + * @return The offset of the first occurrence of the pattern within the specified bounds, or -1 if no match is found. + */ + int indexOf(Pattern pattern); + + /** + * Searches the data in the ByteArray for the first occurrence of a specified pattern. + * + * @param pattern The pattern to be matched. + * @param startIndexInclusive The inclusive start index for the search. + * @param endIndexExclusive The exclusive end index for the search. + * + * @return The offset of the first occurrence of the pattern within the specified bounds, or -1 if no match is found. + */ + int indexOf(Pattern pattern, int startIndexInclusive, int endIndexExclusive); + + /** + * Searches the data in the ByteArray and counts all matches for a specified term. + * + * @param searchTerm The value to be searched for. + * + * @return The count of all matches of the pattern. + */ + int countMatches(ByteArray searchTerm); + + /** + * Searches the data in the ByteArray and counts all matches for a specified term. + * + * @param searchTerm The value to be searched for. + * + * @return The count of all matches of the pattern. + */ + int countMatches(String searchTerm); + + /** + * Searches the data in the ByteArray and counts all matches for a specified term. + * + * @param searchTerm The value to be searched for. + * @param caseSensitive Flags whether the search is case-sensitive. + * + * @return The count of all matches of the pattern. + */ + int countMatches(ByteArray searchTerm, boolean caseSensitive); + + /** + * Searches the data in the ByteArray and counts all matches for a specified term. + * + * @param searchTerm The value to be searched for. + * @param caseSensitive Flags whether the search is case-sensitive. + * + * @return The count of all matches of the pattern. + */ + int countMatches(String searchTerm, boolean caseSensitive); + + /** + * Searches the data in the ByteArray and counts all matches for a specified term. + * + * @param searchTerm The value to be searched for. + * @param caseSensitive Flags whether the search is case-sensitive. + * @param startIndexInclusive The inclusive start index for the search. + * @param endIndexExclusive The exclusive end index for the search. + * + * @return The count of all matches of the pattern within the specified bounds. + */ + int countMatches(ByteArray searchTerm, boolean caseSensitive, int startIndexInclusive, int endIndexExclusive); + + /** + * Searches the data in the ByteArray and counts all matches for a specified term. + * + * @param searchTerm The value to be searched for. + * @param caseSensitive Flags whether the search is case-sensitive. + * @param startIndexInclusive The inclusive start index for the search. + * @param endIndexExclusive The exclusive end index for the search. + * + * @return The count of all matches of the pattern within the specified bounds. + */ + int countMatches(String searchTerm, boolean caseSensitive, int startIndexInclusive, int endIndexExclusive); + + /** + * Searches the data in the ByteArray and counts all matches for a specified pattern. + * + * @param pattern The pattern to be matched. + * + * @return The count of all matches of the pattern within the specified bounds. + */ + int countMatches(Pattern pattern); + + /** + * Searches the data in the ByteArray and counts all matches for a specified pattern. + * + * @param pattern The pattern to be matched. + * @param startIndexInclusive The inclusive start index for the search. + * @param endIndexExclusive The exclusive end index for the search. + * + * @return The count of all matches of the pattern within the specified bounds. + */ + int countMatches(Pattern pattern, int startIndexInclusive, int endIndexExclusive); + + /** + * Convert the bytes of the ByteArray into String form using the encoding specified by Burp Suite. + * + * @return The converted data in String form. + */ + @Override + String toString(); + + /** + * Create a copy of the {@code ByteArray} appended with the provided bytes. + * + * @param data The byte[] or sequence of bytes to append. + */ + ByteArray withAppended(byte... data); + + /** + * Create a copy of the {@code ByteArray} appended with the provided integers after narrowing primitive conversion to bytes. + * + * @param data The int[] or sequence of integers to append after narrowing primitive conversion to bytes. + */ + ByteArray withAppended(int... data); + + /** + * Create a copy of the {@code ByteArray} appended with the provided text as bytes. + * + * @param text The string to append. + */ + ByteArray withAppended(String text); + + /** + * Create a copy of the {@code ByteArray} appended with the provided ByteArray. + * + * @param byteArray The ByteArray to append. + */ + ByteArray withAppended(ByteArray byteArray); + + /** + * Create a new {@code ByteArray} with the provided length.
+ * + * @param length array length. + * + * @return New {@code ByteArray} with the provided length. + */ + static ByteArray byteArrayOfLength(int length) + { + return FACTORY.byteArrayOfLength(length); + } + + /** + * Create a new {@code ByteArray} with the provided byte data.
+ * + * @param data byte[] to wrap, or sequence of bytes to wrap. + * + * @return New {@code ByteArray} wrapping the provided byte array. + */ + static ByteArray byteArray(byte... data) + { + return FACTORY.byteArray(data); + } + + /** + * Create a new {@code ByteArray} with the provided integers after a narrowing primitive conversion to bytes.
+ * + * @param data int[] to wrap or sequence of integers to wrap. + * + * @return New {@code ByteArray} wrapping the provided data after a narrowing primitive conversion to bytes. + */ + static ByteArray byteArray(int... data) + { + return FACTORY.byteArray(data); + } + + /** + * Create a new {@code ByteArray} from the provided text using the encoding specified by Burp Suite.
+ * + * @param text the text for the byte array. + * + * @return New {@code ByteArray} holding a copy of the text as bytes. + */ + static ByteArray byteArray(String text) + { + return FACTORY.byteArray(text); + } +} + diff --git a/src/burp/api/montoya/core/HighlightColor.java b/src/burp/api/montoya/core/HighlightColor.java new file mode 100644 index 0000000..c7ac10c --- /dev/null +++ b/src/burp/api/montoya/core/HighlightColor.java @@ -0,0 +1,55 @@ +/* + * Copyright (c) 2022-2023. PortSwigger Ltd. All rights reserved. + * + * This code may be used to extend the functionality of Burp Suite Community Edition + * and Burp Suite Professional, provided that this usage does not violate the + * license terms for those products. + */ + +package burp.api.montoya.core; + +import static burp.api.montoya.internal.ObjectFactoryLocator.FACTORY; + +/** + * Colors that can be used for highlights in Burp Suite. + */ +public enum HighlightColor +{ + NONE("None"), + RED("Red"), + ORANGE("Orange"), + YELLOW("Yellow"), + GREEN("Green"), + CYAN("Cyan"), + BLUE("Blue"), + PINK("Pink"), + MAGENTA("Magenta"), + GRAY("Gray"); + + private final String displayName; + + HighlightColor(String displayName) + { + this.displayName = displayName; + } + + /** + * @return displayName of highlightColor + */ + public String displayName() + { + return displayName; + } + + /** + * Create HighlightColor from display name string. + * + * @param colorName Color's display name + * + * @return highlight color instance + */ + public static HighlightColor highlightColor(String colorName) + { + return FACTORY.highlightColor(colorName); + } +} diff --git a/src/burp/api/montoya/core/Marker.java b/src/burp/api/montoya/core/Marker.java new file mode 100644 index 0000000..bd9329a --- /dev/null +++ b/src/burp/api/montoya/core/Marker.java @@ -0,0 +1,47 @@ +/* + * Copyright (c) 2022-2023. PortSwigger Ltd. All rights reserved. + * + * This code may be used to extend the functionality of Burp Suite Community Edition + * and Burp Suite Professional, provided that this usage does not violate the + * license terms for those products. + */ + +package burp.api.montoya.core; + +import static burp.api.montoya.internal.ObjectFactoryLocator.FACTORY; + +/** + * Marker containing a range representing interesting data in requests and responses. + */ +public interface Marker +{ + /** + * @return The range of the marker. + */ + Range range(); + + /** + * Create a marker object with a range. + * + * @param range The range of the marker. + * + * @return The marker with the range. + */ + static Marker marker(Range range) + { + return FACTORY.marker(range); + } + + /** + * Create a marker object from two indices representing a range. + * + * @param startIndexInclusive The start index of the range inclusive of this value. + * @param endIndexExclusive The end index of the range exclusive of this value. + * + * @return The marker with the range. + */ + static Marker marker(int startIndexInclusive, int endIndexExclusive) + { + return FACTORY.marker(startIndexInclusive, endIndexExclusive); + } +} diff --git a/src/burp/api/montoya/core/Range.java b/src/burp/api/montoya/core/Range.java new file mode 100644 index 0000000..f16ac2f --- /dev/null +++ b/src/burp/api/montoya/core/Range.java @@ -0,0 +1,47 @@ +/* + * Copyright (c) 2022-2023. PortSwigger Ltd. All rights reserved. + * + * This code may be used to extend the functionality of Burp Suite Community Edition + * and Burp Suite Professional, provided that this usage does not violate the + * license terms for those products. + */ + +package burp.api.montoya.core; + +import static burp.api.montoya.internal.ObjectFactoryLocator.FACTORY; + +/** + * Range of integer values between two values in which the range includes the start value but excludes the end value. + */ +public interface Range +{ + /** + * @return the inclusive start index + */ + int startIndexInclusive(); + + /** + * @return the exclusive end index + */ + int endIndexExclusive(); + + /** + * @param index The index to test. + * + * @return True if the index is in the range. + */ + boolean contains(int index); + + /** + * Create a range object from two indices. + * + * @param startIndexInclusive The start index of the range inclusive of this value. + * @param endIndexExclusive The end index of the range exclusive of this value. + * + * @return The range. + */ + static Range range(int startIndexInclusive, int endIndexExclusive) + { + return FACTORY.range(startIndexInclusive, endIndexExclusive); + } +} diff --git a/src/burp/api/montoya/core/Registration.java b/src/burp/api/montoya/core/Registration.java new file mode 100644 index 0000000..6759c33 --- /dev/null +++ b/src/burp/api/montoya/core/Registration.java @@ -0,0 +1,27 @@ +/* + * Copyright (c) 2022-2023. PortSwigger Ltd. All rights reserved. + * + * This code may be used to extend the functionality of Burp Suite Community Edition + * and Burp Suite Professional, provided that this usage does not violate the + * license terms for those products. + */ + +package burp.api.montoya.core; + +/** + * Returned when an object is registered by an extension in Burp Suite. + */ +public interface Registration +{ + /** + * Determines whether the object registered by the extension is currently registered. + * + * @return Returns {@code true} if the object is registered. + */ + boolean isRegistered(); + + /** + * Remove the object registered by the extension. + */ + void deregister(); +} diff --git a/src/burp/api/montoya/core/Task.java b/src/burp/api/montoya/core/Task.java new file mode 100644 index 0000000..bdc0640 --- /dev/null +++ b/src/burp/api/montoya/core/Task.java @@ -0,0 +1,25 @@ +/* + * Copyright (c) 2022-2023. PortSwigger Ltd. All rights reserved. + * + * This code may be used to extend the functionality of Burp Suite Community Edition + * and Burp Suite Professional, provided that this usage does not violate the + * license terms for those products. + */ + +package burp.api.montoya.core; + +/** + * Task on the Dashboard. + */ +public interface Task +{ + /** + * Delete the task. + */ + void delete(); + + /** + * @return the current status message of the task + */ + String statusMessage(); +} diff --git a/src/burp/api/montoya/core/ToolSource.java b/src/burp/api/montoya/core/ToolSource.java new file mode 100644 index 0000000..3230c22 --- /dev/null +++ b/src/burp/api/montoya/core/ToolSource.java @@ -0,0 +1,30 @@ +/* + * Copyright (c) 2022-2023. PortSwigger Ltd. All rights reserved. + * + * This code may be used to extend the functionality of Burp Suite Community Edition + * and Burp Suite Professional, provided that this usage does not violate the + * license terms for those products. + */ + +package burp.api.montoya.core; + +/** + * Tool that is the source of an object. + */ +public interface ToolSource +{ + /** + * @return the tool type. + */ + ToolType toolType(); + + /** + * Determine whether this tool source is from a specified tool. + * + * @param toolType The tool types to check. + * + * @return Returns {@code true} if this tool source is from any of the + * specified tool types. + */ + boolean isFromTool(ToolType... toolType); +} diff --git a/src/burp/api/montoya/core/ToolType.java b/src/burp/api/montoya/core/ToolType.java new file mode 100644 index 0000000..d4e639f --- /dev/null +++ b/src/burp/api/montoya/core/ToolType.java @@ -0,0 +1,53 @@ +/* + * Copyright (c) 2022-2023. PortSwigger Ltd. All rights reserved. + * + * This code may be used to extend the functionality of Burp Suite Community Edition + * and Burp Suite Professional, provided that this usage does not violate the + * license terms for those products. + */ + +package burp.api.montoya.core; + +/** + * Tools in Burp Suite. + */ +public enum ToolType +{ + SUITE("Suite"), + TARGET("Target"), + PROXY("Proxy"), + SCANNER("Scanner"), + INTRUDER("Intruder"), + REPEATER("Repeater"), + LOGGER("Logger"), + SEQUENCER("Sequencer"), + DECODER("Decoder"), + COMPARER("Comparer"), + EXTENSIONS("Extensions"), + RECORDED_LOGIN_REPLAYER("Recorded login replayer"), + ORGANIZER("Organizer"); + + private final String toolName; + + ToolType(String toolName) + { + this.toolName = toolName; + } + + /** + * @return The tool name. + */ + public String toolName() + { + return toolName; + } + + /** + * @return The tool name. + */ + @Override + public String toString() + { + return toolName; + } +} diff --git a/src/burp/api/montoya/core/Version.java b/src/burp/api/montoya/core/Version.java new file mode 100644 index 0000000..cb1b200 --- /dev/null +++ b/src/burp/api/montoya/core/Version.java @@ -0,0 +1,72 @@ +/* + * Copyright (c) 2022-2023. PortSwigger Ltd. All rights reserved. + * + * This code may be used to extend the functionality of Burp Suite Community Edition + * and Burp Suite Professional, provided that this usage does not violate the + * license terms for those products. + */ + +package burp.api.montoya.core; + +/** + * Product version.
+ * e.g. Burp Suite Professional 2022.8.1-9320 + */ +public interface Version +{ + /** + * The product name (e.g. Burp Suite Professional). + * + * @return The product name. + */ + String name(); + + /** + * The major version (e.g. 2022.8). + * + * @return The major version. + * @deprecated use {@link #toString()} or {@link #buildNumber()} instead. + */ + @Deprecated(forRemoval = true) + String major(); + + /** + * The minor version (e.g. 1). + * + * @return The minor version. + * @deprecated use {@link #toString()} or {@link #buildNumber()} instead. + */ + @Deprecated(forRemoval = true) + String minor(); + + /** + * The build number (e.g. 9320). + * + * @return The build number. + * @deprecated use {@link #toString()} or {@link #buildNumber()} instead. + */ + @Deprecated(forRemoval = true) + String build(); + + /** + * Build number for Burp Suite. You can use this to determine compatibility with different versions of Burp Suite. Do not parse this information, because the format of the number may change. + * + * @return The build number. + */ + long buildNumber(); + + /** + * The edition of Burp Suite + * + * @return The edition of Burp Suite + */ + BurpSuiteEdition edition(); + + /** + * The human-readable version string. Do not parse this information, because the format may change. See also: {@link #buildNumber()}. + * + * @return The human-readable version string. + */ + @Override + String toString(); +} diff --git a/src/burp/api/montoya/decoder/Decoder.java b/src/burp/api/montoya/decoder/Decoder.java new file mode 100644 index 0000000..b46425a --- /dev/null +++ b/src/burp/api/montoya/decoder/Decoder.java @@ -0,0 +1,24 @@ +/* + * Copyright (c) 2022-2023. PortSwigger Ltd. All rights reserved. + * + * This code may be used to extend the functionality of Burp Suite Community Edition + * and Burp Suite Professional, provided that this usage does not violate the + * license terms for those products. + */ + +package burp.api.montoya.decoder; + +import burp.api.montoya.core.ByteArray; + +/** + * Provides access to the functionality of the Decoder tool. + */ +public interface Decoder +{ + /** + * Send data to the Decoder tool. + * + * @param data The data to be sent to Decoder. + */ + void sendToDecoder(ByteArray data); +} diff --git a/src/burp/api/montoya/extension/Extension.java b/src/burp/api/montoya/extension/Extension.java new file mode 100644 index 0000000..5a7b398 --- /dev/null +++ b/src/burp/api/montoya/extension/Extension.java @@ -0,0 +1,61 @@ +/* + * Copyright (c) 2022-2023. PortSwigger Ltd. All rights reserved. + * + * This code may be used to extend the functionality of Burp Suite Community Edition + * and Burp Suite Professional, provided that this usage does not violate the + * license terms for those products. + */ + +package burp.api.montoya.extension; + +import burp.api.montoya.core.Registration; + +/** + * Provides access to functionality related to your Extension. + */ +public interface Extension +{ + /** + * Set the display name for the current extension.
+ * This will be displayed within the user interface for the Extensions tool and + * will be used to identify persisted data. + * + * @param extensionName the name of the extension + */ + void setName(String extensionName); + + /** + * Absolute path name of the file from which the current extension was loaded. + * + * @return The absolute path name of the file from which the current + * extension was loaded. + */ + String filename(); + + /** + * Determines whether the current extension was loaded as a BApp. + * + * @return Returns {@code true} if the current extension was loaded as + * a BApp. + */ + boolean isBapp(); + + /** + * Unload the extension from Burp Suite. + */ + void unload(); + + /** + * Register a handler which will be notified of changes to the extension's state.
+ * Note: Any extensions that start + * background threads or open system resources (such as files or database + * connections) should register a listener and terminate threads / close + * resources when the extension is unloaded. + * + * @param handler An object created by the extension that implements the + * {@link ExtensionUnloadingHandler} interface. + * + * @return The {@link Registration} for the handler. + */ + Registration registerUnloadingHandler(ExtensionUnloadingHandler handler); +} diff --git a/src/burp/api/montoya/extension/ExtensionUnloadingHandler.java b/src/burp/api/montoya/extension/ExtensionUnloadingHandler.java new file mode 100644 index 0000000..f8a8863 --- /dev/null +++ b/src/burp/api/montoya/extension/ExtensionUnloadingHandler.java @@ -0,0 +1,28 @@ +/* + * Copyright (c) 2022-2023. PortSwigger Ltd. All rights reserved. + * + * This code may be used to extend the functionality of Burp Suite Community Edition + * and Burp Suite Professional, provided that this usage does not violate the + * license terms for those products. + */ + +package burp.api.montoya.extension; + + +/** + * Extensions can implement this interface and then call + * {@link Extension#registerUnloadingHandler(ExtensionUnloadingHandler)} to + * register an extension unload handler. The handler will be notified when an + * extension is unloaded.
+ * Note: Any extensions that start background + * threads or open system resources (such as files or database connections) + * should register a handler and terminate threads / close resources when the + * extension is unloaded. + */ +public interface ExtensionUnloadingHandler +{ + /** + * This method is invoked when the extension is unloaded. + */ + void extensionUnloaded(); +} diff --git a/src/burp/api/montoya/http/Http.java b/src/burp/api/montoya/http/Http.java new file mode 100644 index 0000000..e8435d7 --- /dev/null +++ b/src/burp/api/montoya/http/Http.java @@ -0,0 +1,129 @@ +/* + * Copyright (c) 2022-2023. PortSwigger Ltd. All rights reserved. + * + * This code may be used to extend the functionality of Burp Suite Community Edition + * and Burp Suite Professional, provided that this usage does not violate the + * license terms for those products. + */ + +package burp.api.montoya.http; + +import burp.api.montoya.core.Registration; +import burp.api.montoya.http.handler.HttpHandler; +import burp.api.montoya.http.message.HttpRequestResponse; +import burp.api.montoya.http.message.requests.HttpRequest; +import burp.api.montoya.http.message.responses.analysis.ResponseKeywordsAnalyzer; +import burp.api.montoya.http.message.responses.analysis.ResponseVariationsAnalyzer; +import burp.api.montoya.http.sessions.CookieJar; +import burp.api.montoya.http.sessions.SessionHandlingAction; + +import java.util.List; + +/** + * Provides access HTTP related functionality of Burp. + */ +public interface Http +{ + /** + * Register a handler which will perform an action when a request is about to be sent + * or a response was received by any Burp tool. + * + * @param handler An object created by the extension that implements {@link HttpHandler} interface. + * + * @return The {@link Registration} for the handler. + */ + Registration registerHttpHandler(HttpHandler handler); + + /** + * Register a custom session handler. Each registered handler will be available + * within the session handling rule UI for the user to select as a rule action. Users can choose to invoke a + * handler directly in its own right, or following execution of a macro. + * + * @param sessionHandlingAction An object created by the extension that implements {@link SessionHandlingAction} interface. + * + * @return The {@link Registration} for the handler. + */ + Registration registerSessionHandlingAction(SessionHandlingAction sessionHandlingAction); + + /** + * Send HTTP requests and retrieve their responses. + * + * @param request The full HTTP request. + * + * @return An object that implements the {@link HttpRequestResponse} interface, and which the extension can query to obtain the details of the response. + */ + HttpRequestResponse sendRequest(HttpRequest request); + + /** + * Send HTTP requests and retrieve their responses. + * + * @param request The full HTTP request. + * @param httpMode An {@link HttpMode} enum value which indicates how a request should be sent. + * + * @return An object that implements the {@link HttpRequestResponse} interface, and which the extension can query to obtain the details of the response. + */ + HttpRequestResponse sendRequest(HttpRequest request, HttpMode httpMode); + + /** + * Send HTTP requests and retrieve their responses. + * + * @param request The full HTTP request. + * @param httpMode An {@link HttpMode} enum value which indicates how a request should be sent. + * @param connectionId The identifier for the connection you want to use. + * + * @return An object that implements the {@link HttpRequestResponse} interface, and which the extension can query to obtain the details of the response. + */ + HttpRequestResponse sendRequest(HttpRequest request, HttpMode httpMode, String connectionId); + + /** + * Send HTTP request with specific request options and retrieve its response. + * + * @param request The full HTTP request. + * @param requestOptions A {@link RequestOptions} value which indicates how a request should be sent. + * + * @return An object that implements the {@link HttpRequestResponse} interface, and which the extension can query to obtain the details of the response. + */ + HttpRequestResponse sendRequest(HttpRequest request, RequestOptions requestOptions); + + /** + * Send HTTP requests in parallel and retrieve their responses. + * + * @param requests The list of full HTTP requests. + * + * @return A list of objects that implement the {@link HttpRequestResponse} interface, and which the extension can query to obtain the details of the responses. + */ + List sendRequests(List requests); + + /** + * Send HTTP requests in parallel and retrieve their responses. + * + * @param requests The list of full HTTP requests. + * @param httpMode An {@link HttpMode} enum value which indicates how a request should be sent. + * + * @return A list of objects that implement the {@link HttpRequestResponse} interface, and which the extension can query to obtain the details of the responses. + */ + List sendRequests(List requests, HttpMode httpMode); + + /** + * Create a new response keyword analyzer. + * + * @param keywords A list of keywords the analyzer will look for. + * + * @return A new {@link ResponseKeywordsAnalyzer} instance. + */ + ResponseKeywordsAnalyzer createResponseKeywordsAnalyzer(List keywords); + + /** + * Create a new response variations analyzer. + * + * @return A new {@link ResponseKeywordsAnalyzer} instance. + */ + ResponseVariationsAnalyzer createResponseVariationsAnalyzer(); + + /** + * Access the Cookie Jar. + * + * @return The {@link CookieJar} instance. + */ + CookieJar cookieJar(); +} diff --git a/src/burp/api/montoya/http/HttpMode.java b/src/burp/api/montoya/http/HttpMode.java new file mode 100644 index 0000000..b578951 --- /dev/null +++ b/src/burp/api/montoya/http/HttpMode.java @@ -0,0 +1,35 @@ +/* + * Copyright (c) 2022-2023. PortSwigger Ltd. All rights reserved. + * + * This code may be used to extend the functionality of Burp Suite Community Edition + * and Burp Suite Professional, provided that this usage does not violate the + * license terms for those products. + */ + +package burp.api.montoya.http; + +/** + * HTTP modes when sending a request. + */ +public enum HttpMode +{ + /** + * Use the HTTP protocol specified by the server + */ + AUTO, + /** + * Use HTTP 1 protocol for the connection.
+ * Will error if server is HTTP 2 only. + */ + HTTP_1, + /** + * Use HTTP 2 protocol for the connection.
+ * Will error if server is HTTP 1 only. + */ + HTTP_2, + /** + * Force HTTP 2 and ignore ALPN.
+ * Will not error if server is HTTP 1 only. + */ + HTTP_2_IGNORE_ALPN +} diff --git a/src/burp/api/montoya/http/HttpProtocol.java b/src/burp/api/montoya/http/HttpProtocol.java new file mode 100644 index 0000000..cc09300 --- /dev/null +++ b/src/burp/api/montoya/http/HttpProtocol.java @@ -0,0 +1,24 @@ +/* + * Copyright (c) 2022-2023. PortSwigger Ltd. All rights reserved. + * + * This code may be used to extend the functionality of Burp Suite Community Edition + * and Burp Suite Professional, provided that this usage does not violate the + * license terms for those products. + */ + +package burp.api.montoya.http; + +/** + * HTTP protocols. + */ +public enum HttpProtocol +{ + /** + * Hypertext Transfer Protocol + */ + HTTP, + /** + * Hypertext Transfer Protocol Secure + */ + HTTPS +} diff --git a/src/burp/api/montoya/http/HttpService.java b/src/burp/api/montoya/http/HttpService.java new file mode 100644 index 0000000..49eacd7 --- /dev/null +++ b/src/burp/api/montoya/http/HttpService.java @@ -0,0 +1,87 @@ +/* + * Copyright (c) 2022-2023. PortSwigger Ltd. All rights reserved. + * + * This code may be used to extend the functionality of Burp Suite Community Edition + * and Burp Suite Professional, provided that this usage does not violate the + * license terms for those products. + */ + +package burp.api.montoya.http; + +import static burp.api.montoya.internal.ObjectFactoryLocator.FACTORY; + +/** + * Burp HTTP service providing details about an HTTP service, to which HTTP requests can be sent. + */ +public interface HttpService +{ + + /** + * @return The hostname or IP address for the service. + */ + String host(); + + /** + * @return The port number for the service. + */ + int port(); + + /** + * @return True if a secure protocol is used for the connection, false otherwise. + */ + boolean secure(); + + /** + * Dynamically resolve the host to an IP address. + * + * @return The IP address of the host. + */ + String ipAddress(); + + /** + * @return The {@code String} representation of the service. + */ + @Override + String toString(); + + /** + * Create a new instance of {@code HttpService}. + * + * @param baseUrl The URL for the service. + * + * @return A new {@code HttpService} instance. + * + * @throws IllegalArgumentException If the provided URL is invalid. + */ + static HttpService httpService(String baseUrl) + { + return FACTORY.httpService(baseUrl); + } + + /** + * Create a new instance of {@code HttpService}. + * + * @param host The hostname or IP address for the service. + * @param secure True if a secure connection is to be used. + * + * @return A new {@code HttpService} instance. + */ + static HttpService httpService(String host, boolean secure) + { + return FACTORY.httpService(host, secure); + } + + /** + * Create a new instance of {@code HttpService}. + * + * @param host The hostname or IP address for the service. + * @param port The port number for the service. + * @param secure True if a secure connection is to be used. + * + * @return A new {@code HttpService} instance. + */ + static HttpService httpService(String host, int port, boolean secure) + { + return FACTORY.httpService(host, port, secure); + } +} diff --git a/src/burp/api/montoya/http/RequestOptions.java b/src/burp/api/montoya/http/RequestOptions.java new file mode 100644 index 0000000..abed659 --- /dev/null +++ b/src/burp/api/montoya/http/RequestOptions.java @@ -0,0 +1,44 @@ +package burp.api.montoya.http; + +import static burp.api.montoya.internal.ObjectFactoryLocator.FACTORY; + +/** + * Interface used to specify options for making HTTP requests. + */ +public interface RequestOptions +{ + /** + * Specify HTTP mode to be used when request sent. + * + * @param httpMode An {@link HttpMode} enum value which indicates how a request should be sent. + * + * @return request options + */ + RequestOptions withHttpMode(HttpMode httpMode); + + /** + * Specify connectionId when sending request over specific connection. + * + * @param connectionId The connection identifier to use. + * + * @return request options + */ + RequestOptions withConnectionId(String connectionId); + + /** + * Enforce upstream TLS verification when request sent. + * + * @return request options + */ + RequestOptions withUpstreamTLSVerification(); + + /** + * Use to obtain a new RequestOptions instance + * + * @return request options + */ + static RequestOptions requestOptions() + { + return FACTORY.requestOptions(); + } +} diff --git a/src/burp/api/montoya/http/handler/HttpHandler.java b/src/burp/api/montoya/http/handler/HttpHandler.java new file mode 100644 index 0000000..925d076 --- /dev/null +++ b/src/burp/api/montoya/http/handler/HttpHandler.java @@ -0,0 +1,37 @@ +/* + * Copyright (c) 2022-2023. PortSwigger Ltd. All rights reserved. + * + * This code may be used to extend the functionality of Burp Suite Community Edition + * and Burp Suite Professional, provided that this usage does not violate the + * license terms for those products. + */ + +package burp.api.montoya.http.handler; + +import burp.api.montoya.http.Http; + +/** + * Extensions can implement this interface and then call {@link Http#registerHttpHandler} to register an HTTP handler. The handler + * will be notified of requests and responses made and received by any Burp tool. Extensions can perform custom analysis or modification + * of these messages by registering an HTTP handler. + */ +public interface HttpHandler +{ + /** + * Invoked by Burp when an HTTP request is about to be sent. + * + * @param requestToBeSent information about the HTTP request that is going to be sent. + * + * @return An instance of {@link RequestToBeSentAction}. + */ + RequestToBeSentAction handleHttpRequestToBeSent(HttpRequestToBeSent requestToBeSent); + + /** + * Invoked by Burp when an HTTP response has been received. + * + * @param responseReceived information about HTTP response that was received. + * + * @return An instance of {@link ResponseReceivedAction}. + */ + ResponseReceivedAction handleHttpResponseReceived(HttpResponseReceived responseReceived); +} diff --git a/src/burp/api/montoya/http/handler/HttpRequestToBeSent.java b/src/burp/api/montoya/http/handler/HttpRequestToBeSent.java new file mode 100644 index 0000000..151ff5a --- /dev/null +++ b/src/burp/api/montoya/http/handler/HttpRequestToBeSent.java @@ -0,0 +1,554 @@ +/* + * Copyright (c) 2022-2023. PortSwigger Ltd. All rights reserved. + * + * This code may be used to extend the functionality of Burp Suite Community Edition + * and Burp Suite Professional, provided that this usage does not violate the + * license terms for those products. + */ + +package burp.api.montoya.http.handler; + +import burp.api.montoya.core.Annotations; +import burp.api.montoya.core.ByteArray; +import burp.api.montoya.core.Marker; +import burp.api.montoya.core.ToolSource; +import burp.api.montoya.http.HttpService; +import burp.api.montoya.http.message.ContentType; +import burp.api.montoya.http.message.HttpHeader; +import burp.api.montoya.http.message.params.HttpParameter; +import burp.api.montoya.http.message.params.HttpParameterType; +import burp.api.montoya.http.message.params.ParsedHttpParameter; +import burp.api.montoya.http.message.requests.HttpRequest; +import burp.api.montoya.http.message.requests.HttpTransformation; +import burp.api.montoya.http.message.requests.MalformedRequestException; + +import java.util.List; +import java.util.regex.Pattern; + +/** + * Burp {@link HttpRequest} with additional methods to retrieve {@link Annotations} and {@link ToolSource} of the request. + */ +public interface HttpRequestToBeSent extends HttpRequest +{ + /** + * @return The ID for this request to be sent. The corresponding response will have an identical ID. + */ + int messageId(); + + /** + * @return annotations for request/response + */ + Annotations annotations(); + + /** + * @return Indicates which Burp tool sent the request. + */ + ToolSource toolSource(); + + /** + * @return True if the request is in-scope. + */ + @Override + boolean isInScope(); + + /** + * HTTP service for the request. + * + * @return An {@link HttpService} object containing details of the HTTP service. + */ + @Override + HttpService httpService(); + + /** + * URL for the request. + * If the request is malformed, then a {@link MalformedRequestException} is thrown. + * + * @return The URL in the request. + * @throws MalformedRequestException if request is malformed. + */ + @Override + String url(); + + /** + * HTTP method for the request. + * If the request is malformed, then a {@link MalformedRequestException} is thrown. + * + * @return The HTTP method used in the request. + * @throws MalformedRequestException if request is malformed. + */ + @Override + String method(); + + /** + * Request path including the query parameters. + * If the request is malformed, then a {@link MalformedRequestException} is thrown. + * + * @return the path and query parameters. + * @throws MalformedRequestException if request is malformed. + */ + @Override + String path(); + + /** + * Request path excluding the query parameters. + * If the request is malformed, then a {@link MalformedRequestException} is thrown. + * + * @return the path excluding query parameters. + * @throws MalformedRequestException if request is malformed. + */ + @Override + String pathWithoutQuery(); + + /** + * HTTP Version text parsed from the request line for HTTP 1 messages. + * HTTP 2 messages will return "HTTP/2" + * + * @return Version string + */ + @Override + String httpVersion(); + + /** + * HTTP headers contained in the message. + * + * @return A list of HTTP headers. + */ + @Override + List headers(); + + /** + * @param header The header to check if it exists in the request. + * + * @return True if the header exists in the request. + */ + @Override + boolean hasHeader(HttpHeader header); + + /** + * @param name The name of the header to query within the request. + * + * @return True if a header exists in the request with the supplied name. + */ + @Override + boolean hasHeader(String name); + + /** + * @param name The name of the header to check. + * @param value The value of the header to check. + * + * @return True if a header exists in the request that matches the name and value supplied. + */ + @Override + boolean hasHeader(String name, String value); + + /** + * @param name The name of the header to retrieve. + * + * @return An instance of {@link HttpHeader} that matches the name supplied, {@code null} if no match found. + */ + @Override + HttpHeader header(String name); + + /** + * @param name The name of the header to retrieve. + * + * @return The {@code String} value of the header that matches the name supplied, {@code null} if no match found. + */ + @Override + String headerValue(String name); + + /** + * @return True if the request has parameters. + */ + @Override + boolean hasParameters(); + + /** + * @return True if the request has parameters of type {@link HttpParameterType} + */ + @Override + boolean hasParameters(HttpParameterType type); + + /** + * @param name The name of the parameter to find. + * @param type The type of the parameter to find. + * + * @return An instance of {@link ParsedHttpParameter} that matches the type and name specified. {@code null} if not found. + */ + @Override + ParsedHttpParameter parameter(String name, HttpParameterType type); + + /** + * @param name The name of the parameter to get the value from. + * @param type The type of the parameter to get the value from. + * + * @return The value of the parameter that matches the name and type specified. {@code null} if not found. + */ + @Override + String parameterValue(String name, HttpParameterType type); + + /** + * @param name The name of the parameter to find. + * @param type The type of the parameter to find. + * + * @return {@code true} if a parameter exists that matches the name and type specified. {@code false} if not found. + */ + @Override + boolean hasParameter(String name, HttpParameterType type); + + /** + * @param parameter An instance of {@link HttpParameter} to match to an existing parameter. + * + * @return {@code true} if a parameter exists that matches the data within the provided {@link HttpParameter}. {@code false} if not found. + */ + @Override + boolean hasParameter(HttpParameter parameter); + + /** + * @return The detected content type of the request. + */ + @Override + ContentType contentType(); + + /** + * @return The parameters contained in the request. + */ + @Override + List parameters(); + + /** + * @param type The type of parameter that will be returned in the filtered list. + * + * @return A filtered list of {@link ParsedHttpParameter} containing only the provided type. + */ + @Override + List parameters(HttpParameterType type); + + /** + * Body of a message as a byte array. + * + * @return The body of a message as a byte array. + */ + @Override + ByteArray body(); + + /** + * Body of a message as a {@code String}. + * + * @return The body of a message as a {@code String}. + */ + @Override + String bodyToString(); + + /** + * Offset within the message where the message body begins. + * + * @return The message body offset. + */ + @Override + int bodyOffset(); + + /** + * Markers for the message. + * + * @return A list of markers. + */ + @Override + List markers(); + + /** + * Searches the data in the HTTP message for the specified search term. + * + * @param searchTerm The value to be searched for. + * @param caseSensitive Flags whether the search is case-sensitive. + * + * @return True if the search term is found. + */ + @Override + boolean contains(String searchTerm, boolean caseSensitive); + + /** + * Searches the data in the HTTP message for the specified regular expression. + * + * @param pattern The regular expression to be searched for. + * + * @return True if the pattern is matched. + */ + @Override + boolean contains(Pattern pattern); + + /** + * Message as a byte array. + * + * @return The message as a byte array. + */ + @Override + ByteArray toByteArray(); + + /** + * Message as a {@code String}. + * + * @return The message as a {@code String}. + */ + @Override + String toString(); + + /** + * Create a copy of the {@code HttpRequest} in temporary file.
+ * This method is used to save the {@code HttpRequest} object to a temporary file, + * so that it is no longer held in memory. Extensions can use this method to convert + * {@code HttpRequest} objects into a form suitable for long-term usage. + * + * @return A new {@code HttpRequest} instance stored in temporary file. + */ + HttpRequest copyToTempFile(); + + /** + * Create a copy of the {@code HttpRequest} with the new service. + * + * @param service An {@link HttpService} reference to add. + * + * @return A new {@code HttpRequest} instance. + */ + @Override + HttpRequest withService(HttpService service); + + /** + * Create a copy of the {@code HttpRequest} with the new path. + * + * @param path The path to use. + * + * @return A new {@code HttpRequest} instance with updated path. + */ + @Override + HttpRequest withPath(String path); + + /** + * Create a copy of the {@code HttpRequest} with the new method. + * + * @param method the method to use + * + * @return a new {@code HttpRequest} instance with updated method. + */ + @Override + HttpRequest withMethod(String method); + + /** + * Create a copy of the {@code HttpRequest} with the added or updated header.
+ * If the header exists in the request, it is updated.
+ * If the header doesn't exist in the request, it is added. + * + * @param header HTTP header to add or update. + * + * @return A new {@code HttpRequest} with the added or updated header. + */ + @Override + HttpRequest withHeader(HttpHeader header); + + /** + * Create a copy of the {@code HttpRequest} with the added or updated header.
+ * If the header exists in the request, it is updated.
+ * If the header doesn't exist in the request, it is added. + * + * @param name The name of the header. + * @param value The value of the header. + * + * @return A new {@code HttpRequest} with the added or updated header. + */ + @Override + HttpRequest withHeader(String name, String value); + + /** + * Create a copy of the {@code HttpRequest} with the HTTP parameter.
+ * If the parameter exists in the request, it is updated.
+ * If the parameter doesn't exist in the request, it is added. + * + * @param parameters HTTP parameter to add or update. + * + * @return A new {@code HttpRequest} with the added or updated parameter. + */ + @Override + HttpRequest withParameter(HttpParameter parameters); + + /** + * Create a copy of the {@code HttpRequest} with the added HTTP parameters. + * + * @param parameters HTTP parameters to add. + * + * @return A new {@code HttpRequest} instance. + */ + @Override + HttpRequest withAddedParameters(List parameters); + + /** + * Create a copy of the {@code HttpRequest} with the added HTTP parameters. + * + * @param parameters HTTP parameters to add. + * + * @return A new {@code HttpRequest} instance. + */ + @Override + HttpRequest withAddedParameters(HttpParameter... parameters); + + /** + * Create a copy of the {@code HttpRequest} with the removed HTTP parameters. + * + * @param parameters HTTP parameters to remove. + * + * @return A new {@code HttpRequest} instance. + */ + @Override + HttpRequest withRemovedParameters(List parameters); + + /** + * Create a copy of the {@code HttpRequest} with the removed HTTP parameters. + * + * @param parameters HTTP parameters to remove. + * + * @return A new {@code HttpRequest} instance. + */ + @Override + HttpRequest withRemovedParameters(HttpParameter... parameters); + + /** + * Create a copy of the {@code HttpRequest} with the updated HTTP parameters.
+ * + * @param parameters HTTP parameters to update. + * + * @return A new {@code HttpRequest} instance. + */ + @Override + HttpRequest withUpdatedParameters(List parameters); + + /** + * Create a copy of the {@code HttpRequest} with the updated HTTP parameters.
+ * + * @param parameters HTTP parameters to update. + * + * @return A new {@code HttpRequest} instance. + */ + @Override + HttpRequest withUpdatedParameters(HttpParameter... parameters); + + /** + * Create a copy of the {@code HttpRequest} with the transformation applied. + * + * @param transformation Transformation to apply. + * + * @return A new {@code HttpRequest} instance. + */ + @Override + HttpRequest withTransformationApplied(HttpTransformation transformation); + + /** + * Create a copy of the {@code HttpRequest} with the updated body.
+ * Updates Content-Length header. + * + * @param body the new body for the request + * + * @return A new {@code HttpRequest} instance. + */ + @Override + HttpRequest withBody(String body); + + /** + * Create a copy of the {@code HttpRequest} with the updated body.
+ * Updates Content-Length header. + * + * @param body the new body for the request + * + * @return A new {@code HttpRequest} instance. + */ + @Override + HttpRequest withBody(ByteArray body); + + /** + * Create a copy of the {@code HttpRequest} with the added header. + * + * @param name The name of the header. + * @param value The value of the header. + * + * @return The updated HTTP request with the added header. + */ + @Override + HttpRequest withAddedHeader(String name, String value); + + /** + * Create a copy of the {@code HttpRequest} with the added header. + * + * @param header The {@link HttpHeader} to add to the HTTP request. + * + * @return The updated HTTP request with the added header. + */ + @Override + HttpRequest withAddedHeader(HttpHeader header); + + /** + * Create a copy of the {@code HttpRequest} with the updated header. + * + * @param name The name of the header to update the value of. + * @param value The new value of the specified HTTP header. + * + * @return The updated request containing the updated header. + */ + @Override + HttpRequest withUpdatedHeader(String name, String value); + + /** + * Create a copy of the {@code HttpRequest} with the updated header. + * + * @param header The {@link HttpHeader} to update containing the new value. + * + * @return The updated request containing the updated header. + */ + @Override + HttpRequest withUpdatedHeader(HttpHeader header); + + /** + * Removes an existing HTTP header from the current request. + * + * @param name The name of the HTTP header to remove from the request. + * + * @return The updated request containing the removed header. + */ + @Override + HttpRequest withRemovedHeader(String name); + + /** + * Removes an existing HTTP header from the current request. + * + * @param header The {@link HttpHeader} to remove from the request. + * + * @return The updated request containing the removed header. + */ + @Override + HttpRequest withRemovedHeader(HttpHeader header); + + /** + * Create a copy of the {@code HttpRequest} with the added markers. + * + * @param markers Request markers to add. + * + * @return A new {@link HttpRequest} instance. + */ + @Override + HttpRequest withMarkers(List markers); + + /** + * Create a copy of the {@code HttpRequest} with the added markers. + * + * @param markers Request markers to add. + * + * @return A new {@link HttpRequest} instance. + */ + @Override + HttpRequest withMarkers(Marker... markers); + + /** + * Create a copy of the {@code HttpRequest} with added default headers. + * + * @return a new {@link HttpRequest} with added default headers + */ + @Override + HttpRequest withDefaultHeaders(); +} diff --git a/src/burp/api/montoya/http/handler/HttpResponseReceived.java b/src/burp/api/montoya/http/handler/HttpResponseReceived.java new file mode 100644 index 0000000..5600f86 --- /dev/null +++ b/src/burp/api/montoya/http/handler/HttpResponseReceived.java @@ -0,0 +1,424 @@ +/* + * Copyright (c) 2022-2023. PortSwigger Ltd. All rights reserved. + * + * This code may be used to extend the functionality of Burp Suite Community Edition + * and Burp Suite Professional, provided that this usage does not violate the + * license terms for those products. + */ + +package burp.api.montoya.http.handler; + +import burp.api.montoya.core.Annotations; +import burp.api.montoya.core.ByteArray; +import burp.api.montoya.core.Marker; +import burp.api.montoya.core.ToolSource; +import burp.api.montoya.http.message.Cookie; +import burp.api.montoya.http.message.HttpHeader; +import burp.api.montoya.http.message.MimeType; +import burp.api.montoya.http.message.StatusCodeClass; +import burp.api.montoya.http.message.requests.HttpRequest; +import burp.api.montoya.http.message.responses.HttpResponse; +import burp.api.montoya.http.message.responses.analysis.Attribute; +import burp.api.montoya.http.message.responses.analysis.AttributeType; +import burp.api.montoya.http.message.responses.analysis.KeywordCount; + +import java.util.List; +import java.util.regex.Pattern; + +/** + * Burp {@link HttpResponse} with additional methods to retrieve initiating {@link HttpRequest} as well as the {@link Annotations} and {@link ToolSource} of the request. + */ +public interface HttpResponseReceived extends HttpResponse +{ + /** + * @return The ID for this response which is identical to the ID on the corresponding request. + */ + int messageId(); + + /** + * @return initiatingRequest The HTTP request that was sent. + */ + HttpRequest initiatingRequest(); + + /** + * @return Annotations for request/response. + */ + Annotations annotations(); + + /** + * @return ToolSource which indicates which Burp tool sent the request. + */ + ToolSource toolSource(); + + /** + * Obtain the HTTP status code contained in the response. + * + * @return HTTP status code. + */ + @Override + short statusCode(); + + /** + * Obtain the HTTP reason phrase contained in the response for HTTP 1 messages. + * HTTP 2 messages will return a mapped phrase based on the status code. + * + * @return HTTP Reason phrase. + */ + @Override + String reasonPhrase(); + + /** + * Test whether the status code is in the specified class. + * + * @param statusCodeClass The class of status code to test. + * + * @return True if the status code is in the class. + */ + @Override + boolean isStatusCodeClass(StatusCodeClass statusCodeClass); + + /** + * Return the HTTP Version text parsed from the response line for HTTP 1 messages. + * HTTP 2 messages will return "HTTP/2" + * + * @return Version string + */ + @Override + String httpVersion(); + + /** + * HTTP headers contained in the message. + * + * @return A list of HTTP headers. + */ + @Override + List headers(); + + /** + * Offset within the message where the message body begins. + * + * @return The message body offset. + */ + @Override + boolean hasHeader(HttpHeader header); + + /** + * @param name The name of the header to query within the request. + * + * @return True if a header exists in the request with the supplied name. + */ + @Override + boolean hasHeader(String name); + + /** + * @param name The name of the header to check. + * @param value The value of the header to check. + * + * @return True if a header exists in the request that matches the name and value supplied. + */ + @Override + boolean hasHeader(String name, String value); + + /** + * @param name The name of the header to retrieve. + * + * @return An instance of {@link HttpHeader} that matches the name supplied, {@code null} if no match found. + */ + @Override + HttpHeader header(String name); + + /** + * @param name The name of the header to retrieve. + * + * @return The {@code String} value of the header that matches the name supplied, {@code null} if no match found. + */ + @Override + String headerValue(String name); + + /** + * Body of a message as a byte array. + * + * @return The body of a message as a byte array. + */ + @Override + ByteArray body(); + + /** + * Body of a message as a {@code String}. + * + * @return The body of a message as a {@code String}. + */ + @Override + String bodyToString(); + + /** + * Offset within the message where the message body begins. + * + * @return The message body offset. + */ + @Override + int bodyOffset(); + + /** + * Markers for the message. + * + * @return A list of markers. + */ + @Override + List markers(); + + /** + * Obtain details of the HTTP cookies set in the response. + * + * @return A list of {@link Cookie} objects representing the cookies set in the response, if any. + */ + @Override + List cookies(); + + /** + * @param name The name of the cookie to find. + * + * @return An instance of {@link Cookie} that matches the name provided. {@code null} if not found. + */ + @Override + Cookie cookie(String name); + + /** + * @param name The name of the cookie to retrieve the value from. + * + * @return The value of the cookie that matches the name provided. {@code null} if not found. + */ + @Override + String cookieValue(String name); + + /** + * @param name The name of the cookie to check if it exists in the response. + * + * @return {@code true} If a cookie exists within the response that matches the name provided. {@code false} if not. + */ + @Override + boolean hasCookie(String name); + + /** + * @param cookie An instance of {@link Cookie} to check if it exists in the response. + * + * @return {@code true} If a cookie exists within the response that matches the {@link Cookie} provided. {@code false} if not. + */ + @Override + boolean hasCookie(Cookie cookie); + + /** + * Obtain the MIME type of the response, as determined by Burp Suite. + * + * @return The MIME type. + */ + @Override + MimeType mimeType(); + + /** + * Obtain the MIME type of the response, as stated in the HTTP headers. + * + * @return The stated MIME type. + */ + @Override + MimeType statedMimeType(); + + /** + * Obtain the MIME type of the response, as inferred from the contents of the HTTP message body. + * + * @return The inferred MIME type. + */ + @Override + MimeType inferredMimeType(); + + /** + * Retrieve the number of types given keywords appear in the response. + * + * @param keywords Keywords to count. + * + * @return List of keyword counts in the order they were provided. + */ + @Override + List keywordCounts(String... keywords); + + /** + * Retrieve the values of response attributes. + * + * @param types Response attributes to retrieve values for. + * + * @return List of {@link Attribute} objects. + */ + @Override + List attributes(AttributeType... types); + + /** + * Searches the data in the HTTP message for the specified search term. + * + * @param searchTerm The value to be searched for. + * @param caseSensitive Flags whether the search is case-sensitive. + * + * @return True if the search term is found. + */ + @Override + boolean contains(String searchTerm, boolean caseSensitive); + + /** + * Searches the data in the HTTP message for the specified regular expression. + * + * @param pattern The regular expression to be searched for. + * + * @return True if the pattern is matched. + */ + @Override + boolean contains(Pattern pattern); + + /** + * Message as a byte array. + * + * @return The message as a byte array. + */ + @Override + ByteArray toByteArray(); + + /** + * Message as a {@code String}. + * + * @return The message as a {@code String}. + */ + @Override + String toString(); + + /** + * Create a copy of the {@code HttpResponse} with the provided status code. + * + * @param statusCode the new status code for response + * + * @return A new {@code HttpResponse} instance. + */ + @Override + HttpResponse withStatusCode(short statusCode); + + /** + * Create a copy of the {@code HttpResponse} with the new reason phrase. + * + * @param reasonPhrase the new reason phrase for response + * + * @return A new {@code HttpResponse} instance. + */ + @Override + HttpResponse withReasonPhrase(String reasonPhrase); + + /** + * Create a copy of the {@code HttpResponse} with the new http version. + * + * @param httpVersion the new http version for response + * + * @return A new {@code HttpResponse} instance. + */ + @Override + HttpResponse withHttpVersion(String httpVersion); + + /** + * Create a copy of the {@code HttpResponse} with the updated body.
+ * Updates Content-Length header. + * + * @param body the new body for the response + * + * @return A new {@code HttpResponse} instance. + */ + @Override + HttpResponse withBody(String body); + + /** + * Create a copy of the {@code HttpResponse} with the updated body.
+ * Updates Content-Length header. + * + * @param body the new body for the response + * + * @return A new {@code HttpResponse} instance. + */ + @Override + HttpResponse withBody(ByteArray body); + + /** + * Create a copy of the {@code HttpResponse} with the added header. + * + * @param header The {@link HttpHeader} to add to the response. + * + * @return The updated response containing the added header. + */ + @Override + HttpResponse withAddedHeader(HttpHeader header); + + /** + * Create a copy of the {@code HttpResponse} with the added header. + * + * @param name The name of the header. + * @param value The value of the header. + * + * @return The updated response containing the added header. + */ + @Override + HttpResponse withAddedHeader(String name, String value); + + /** + * Create a copy of the {@code HttpResponse} with the updated header. + * + * @param header The {@link HttpHeader} to update containing the new value. + * + * @return The updated response containing the updated header. + */ + @Override + HttpResponse withUpdatedHeader(HttpHeader header); + + /** + * Create a copy of the {@code HttpResponse} with the updated header. + * + * @param name The name of the header to update the value of. + * @param value The new value of the specified HTTP header. + * + * @return The updated response containing the updated header. + */ + @Override + HttpResponse withUpdatedHeader(String name, String value); + + /** + * Create a copy of the {@code HttpResponse} with the removed header. + * + * @param header The {@link HttpHeader} to remove from the response. + * + * @return The updated response containing the removed header. + */ + @Override + HttpResponse withRemovedHeader(HttpHeader header); + + /** + * Create a copy of the {@code HttpResponse} with the removed header. + * + * @param name The name of the HTTP header to remove from the response. + * + * @return The updated response containing the removed header. + */ + @Override + HttpResponse withRemovedHeader(String name); + + /** + * Create a copy of the {@code HttpResponse} with the added markers. + * + * @param markers Request markers to add. + * + * @return A new {@code MarkedHttpRequestResponse} instance. + */ + @Override + HttpResponse withMarkers(List markers); + + /** + * Create a copy of the {@code HttpResponse} with the added markers. + * + * @param markers Request markers to add. + * + * @return A new {@code MarkedHttpRequestResponse} instance. + */ + @Override + HttpResponse withMarkers(Marker... markers); +} diff --git a/src/burp/api/montoya/http/handler/RequestAction.java b/src/burp/api/montoya/http/handler/RequestAction.java new file mode 100644 index 0000000..fd25943 --- /dev/null +++ b/src/burp/api/montoya/http/handler/RequestAction.java @@ -0,0 +1,20 @@ +/* + * Copyright (c) 2022-2023. PortSwigger Ltd. All rights reserved. + * + * This code may be used to extend the functionality of Burp Suite Community Edition + * and Burp Suite Professional, provided that this usage does not violate the + * license terms for those products. + */ + +package burp.api.montoya.http.handler; + +/** + * Action to be taken when intercepting HTTP requests. + */ +public enum RequestAction +{ + /** + * Causes Burp to send the request. + */ + CONTINUE +} diff --git a/src/burp/api/montoya/http/handler/RequestToBeSentAction.java b/src/burp/api/montoya/http/handler/RequestToBeSentAction.java new file mode 100644 index 0000000..eea31e6 --- /dev/null +++ b/src/burp/api/montoya/http/handler/RequestToBeSentAction.java @@ -0,0 +1,63 @@ +/* + * Copyright (c) 2022-2023. PortSwigger Ltd. All rights reserved. + * + * This code may be used to extend the functionality of Burp Suite Community Edition + * and Burp Suite Professional, provided that this usage does not violate the + * license terms for those products. + */ + +package burp.api.montoya.http.handler; + +import burp.api.montoya.core.Annotations; +import burp.api.montoya.http.message.requests.HttpRequest; + +import static burp.api.montoya.internal.ObjectFactoryLocator.FACTORY; + +/** + * An instance of this interface should be returned by {@link HttpHandler#handleHttpRequestToBeSent} if a custom {@link HttpHandler} has been registered with Burp. + */ +public interface RequestToBeSentAction +{ + /** + * @return the action. + */ + default RequestAction action() + { + return RequestAction.CONTINUE; + } + + /** + * @return The HTTP request. + */ + HttpRequest request(); + + /** + * @return The annotations. + */ + Annotations annotations(); + + /** + * Create a new instance of {@code RequestResult}. Annotations will not be modified. + * + * @param request An HTTP request. + * + * @return A new {@code RequestHandlerResult} instance. + */ + static RequestToBeSentAction continueWith(HttpRequest request) + { + return FACTORY.requestResult(request); + } + + /** + * Create a new instance of {@code RequestResult}. + * + * @param request An HTTP request. + * @param annotations modified annotations. + * + * @return A new {@code RequestHandlerResult} instance. + */ + static RequestToBeSentAction continueWith(HttpRequest request, Annotations annotations) + { + return FACTORY.requestResult(request, annotations); + } +} diff --git a/src/burp/api/montoya/http/handler/ResponseAction.java b/src/burp/api/montoya/http/handler/ResponseAction.java new file mode 100644 index 0000000..2f8dac5 --- /dev/null +++ b/src/burp/api/montoya/http/handler/ResponseAction.java @@ -0,0 +1,20 @@ +/* + * Copyright (c) 2022-2023. PortSwigger Ltd. All rights reserved. + * + * This code may be used to extend the functionality of Burp Suite Community Edition + * and Burp Suite Professional, provided that this usage does not violate the + * license terms for those products. + */ + +package burp.api.montoya.http.handler; + +/** + * Action to be taken when intercepting HTTP responses. + */ +public enum ResponseAction +{ + /** + * Causes Burp to send the response. + */ + CONTINUE +} diff --git a/src/burp/api/montoya/http/handler/ResponseReceivedAction.java b/src/burp/api/montoya/http/handler/ResponseReceivedAction.java new file mode 100644 index 0000000..8a7562d --- /dev/null +++ b/src/burp/api/montoya/http/handler/ResponseReceivedAction.java @@ -0,0 +1,63 @@ +/* + * Copyright (c) 2022-2023. PortSwigger Ltd. All rights reserved. + * + * This code may be used to extend the functionality of Burp Suite Community Edition + * and Burp Suite Professional, provided that this usage does not violate the + * license terms for those products. + */ + +package burp.api.montoya.http.handler; + +import burp.api.montoya.core.Annotations; +import burp.api.montoya.http.message.responses.HttpResponse; + +import static burp.api.montoya.internal.ObjectFactoryLocator.FACTORY; + +/** + * An instance of this interface should be returned by {@link HttpHandler#handleHttpResponseReceived} if a custom {@link HttpHandler} has been registered with Burp. + */ +public interface ResponseReceivedAction +{ + /** + * @return the action. + */ + default ResponseAction action() + { + return ResponseAction.CONTINUE; + } + + /** + * @return The HTTP response. + */ + HttpResponse response(); + + /** + * @return The annotations. + */ + Annotations annotations(); + + /** + * Create a new instance of {@code ResponseResult}. Annotations will not be modified. + * + * @param response An HTTP response. + * + * @return A new {@code ResponseResult} instance. + */ + static ResponseReceivedAction continueWith(HttpResponse response) + { + return FACTORY.responseResult(response); + } + + /** + * Create a new instance of {@code ResponseResult}. + * + * @param response An HTTP response. + * @param annotations modified annotations. + * + * @return A new {@code ResponseResult} instance. + */ + static ResponseReceivedAction continueWith(HttpResponse response, Annotations annotations) + { + return FACTORY.responseResult(response, annotations); + } +} diff --git a/src/burp/api/montoya/http/handler/TimingData.java b/src/burp/api/montoya/http/handler/TimingData.java new file mode 100644 index 0000000..9e3e304 --- /dev/null +++ b/src/burp/api/montoya/http/handler/TimingData.java @@ -0,0 +1,39 @@ +/* + * Copyright (c) 2022-2023. PortSwigger Ltd. All rights reserved. + * + * This code may be used to extend the functionality of Burp Suite Community Edition + * and Burp Suite Professional, provided that this usage does not violate the + * license terms for those products. + */ + +package burp.api.montoya.http.handler; + +import java.time.Duration; +import java.time.ZonedDateTime; + +/** + * Timing data + */ +public interface TimingData +{ + /** + * The time between when Burp sent the request and the start of the response being received. + * + * @return the duration or null if no response returned. + */ + Duration timeBetweenRequestSentAndStartOfResponse(); + + /** + * The time between when Burp sent the request and the end of the response being received. + * + * @return the duration or null if no response returned or the response never completes. + */ + Duration timeBetweenRequestSentAndEndOfResponse(); + + /** + * The time that Burp issued the request. + * + * @return the time that Burp issued the request. + */ + ZonedDateTime timeRequestSent(); +} \ No newline at end of file diff --git a/src/burp/api/montoya/http/message/ContentType.java b/src/burp/api/montoya/http/message/ContentType.java new file mode 100644 index 0000000..4930d24 --- /dev/null +++ b/src/burp/api/montoya/http/message/ContentType.java @@ -0,0 +1,23 @@ +/* + * Copyright (c) 2022-2023. PortSwigger Ltd. All rights reserved. + * + * This code may be used to extend the functionality of Burp Suite Community Edition + * and Burp Suite Professional, provided that this usage does not violate the + * license terms for those products. + */ + +package burp.api.montoya.http.message; + +/** + * Content types recognised by Burp. + */ +public enum ContentType +{ + NONE, + UNKNOWN, + AMF, + JSON, + MULTIPART, + URL_ENCODED, + XML +} diff --git a/src/burp/api/montoya/http/message/Cookie.java b/src/burp/api/montoya/http/message/Cookie.java new file mode 100644 index 0000000..eb76c2e --- /dev/null +++ b/src/burp/api/montoya/http/message/Cookie.java @@ -0,0 +1,54 @@ +/* + * Copyright (c) 2022-2023. PortSwigger Ltd. All rights reserved. + * + * This code may be used to extend the functionality of Burp Suite Community Edition + * and Burp Suite Professional, provided that this usage does not violate the + * license terms for those products. + */ + +package burp.api.montoya.http.message; + +import burp.api.montoya.http.message.responses.HttpResponse; + +import java.time.ZonedDateTime; +import java.util.Optional; + +/** + * Burp cookie able to retrieve and hold details about a cookie. + */ +public interface Cookie +{ + /** + * @return The name of the cookie + */ + String name(); + + /** + * @return The value of the cookie. + */ + String value(); + + /** + * Domain for which the cookie is in scope.
+ * Note: For cookies that have been obtained from generated responses + * (by calling {@link HttpResponse#httpResponse} and then {@link HttpResponse#cookies}), the domain will be {@code null} if the response + * did not explicitly set a domain attribute for the cookie. + * + * @return The domain for which the cookie is in scope. + */ + String domain(); + + /** + * Path for which the cookie is in scope. + * + * @return The path for which the cookie is in scope or {@code null} if none is set. + */ + String path(); + + /** + * Expiration time for the cookie if available. + * + * @return The expiration time for the cookie (i.e., for non-persistent session cookies). + */ + Optional expiration(); +} diff --git a/src/burp/api/montoya/http/message/HttpHeader.java b/src/burp/api/montoya/http/message/HttpHeader.java new file mode 100644 index 0000000..c80fcea --- /dev/null +++ b/src/burp/api/montoya/http/message/HttpHeader.java @@ -0,0 +1,59 @@ +/* + * Copyright (c) 2022-2023. PortSwigger Ltd. All rights reserved. + * + * This code may be used to extend the functionality of Burp Suite Community Edition + * and Burp Suite Professional, provided that this usage does not violate the + * license terms for those products. + */ + +package burp.api.montoya.http.message; + +import static burp.api.montoya.internal.ObjectFactoryLocator.FACTORY; + +/** + * Burp HTTP header able to retrieve to hold details about an HTTP header. + */ +public interface HttpHeader +{ + /** + * @return The name of the header. + */ + String name(); + + /** + * @return The value of the header. + */ + String value(); + + /** + * @return The {@code String} representation of the header. + */ + @Override + String toString(); + + /** + * Create a new instance of {@code HttpHeader} from name and value. + * + * @param name The name of the header. + * @param value The value of the header. + * + * @return A new {@code HttpHeader} instance. + */ + static HttpHeader httpHeader(String name, String value) + { + return FACTORY.httpHeader(name, value); + } + + /** + * Create a new instance of HttpHeader from a {@code String} header representation. + * It will be parsed according to the HTTP/1.1 specification for headers. + * + * @param header The {@code String} header representation. + * + * @return A new {@code HttpHeader} instance. + */ + static HttpHeader httpHeader(String header) + { + return FACTORY.httpHeader(header); + } +} diff --git a/src/burp/api/montoya/http/message/HttpMessage.java b/src/burp/api/montoya/http/message/HttpMessage.java new file mode 100644 index 0000000..f31cce5 --- /dev/null +++ b/src/burp/api/montoya/http/message/HttpMessage.java @@ -0,0 +1,136 @@ +/* + * Copyright (c) 2022-2023. PortSwigger Ltd. All rights reserved. + * + * This code may be used to extend the functionality of Burp Suite Community Edition + * and Burp Suite Professional, provided that this usage does not violate the + * license terms for those products. + */ + +package burp.api.montoya.http.message; + +import burp.api.montoya.core.ByteArray; +import burp.api.montoya.core.Marker; +import burp.api.montoya.http.message.requests.HttpRequest; +import burp.api.montoya.http.message.responses.HttpResponse; + +import java.util.List; +import java.util.regex.Pattern; + +/** + * Burp message retrieve common information shared by {@link HttpRequest} and {@link HttpResponse}. + */ +public interface HttpMessage +{ + /** + * @param header The header to check if it exists in the request. + * + * @return True if the header exists in the request. + */ + boolean hasHeader(HttpHeader header); + + /** + * @param name The name of the header to query within the request. + * + * @return True if a header exists in the request with the supplied name. + */ + boolean hasHeader(String name); + + /** + * @param name The name of the header to check. + * @param value The value of the header to check. + * + * @return True if a header exists in the request that matches the name and value supplied. + */ + boolean hasHeader(String name, String value); + + /** + * @param name The name of the header to retrieve. + * + * @return An instance of {@link HttpHeader} that matches the name supplied, {@code null} if no match found. + */ + HttpHeader header(String name); + + /** + * @param name The name of the header to retrieve. + * + * @return The {@code String} value of the header that matches the name supplied, {@code null} if no match found. + */ + String headerValue(String name); + + /** + * HTTP headers contained in the message. + * + * @return A list of HTTP headers. + */ + List headers(); + + /** + * HTTP Version text parsed from the request or response line for HTTP 1 messages. + * HTTP 2 messages will return "HTTP/2" + * + * @return Version string + */ + String httpVersion(); + + /** + * Offset within the message where the message body begins. + * + * @return The message body offset. + */ + int bodyOffset(); + + /** + * Body of a message as a byte array. + * + * @return The body of a message as a byte array. + */ + ByteArray body(); + + /** + * Body of a message as a {@code String}. + * + * @return The body of a message as a {@code String}. + */ + String bodyToString(); + + /** + * Markers for the message. + * + * @return A list of markers. + */ + List markers(); + + /** + * Searches the data in the HTTP message for the specified search term. + * + * @param searchTerm The value to be searched for. + * @param caseSensitive Flags whether the search is case-sensitive. + * + * @return True if the search term is found. + */ + boolean contains(String searchTerm, boolean caseSensitive); + + /** + * Searches the data in the HTTP message for the specified regular expression. + * + * @param pattern The regular expression to be searched for. + * + * @return True if the pattern is matched. + */ + boolean contains(Pattern pattern); + + /** + * Message as a byte array. + * + * @return The message as a byte array. + */ + ByteArray toByteArray(); + + /** + * Message as a {@code String}. + * + * @return The message as a {@code String}. + */ + @Override + String toString(); +} diff --git a/src/burp/api/montoya/http/message/HttpRequestResponse.java b/src/burp/api/montoya/http/message/HttpRequestResponse.java new file mode 100644 index 0000000..19288e2 --- /dev/null +++ b/src/burp/api/montoya/http/message/HttpRequestResponse.java @@ -0,0 +1,201 @@ +/* + * Copyright (c) 2022-2023. PortSwigger Ltd. All rights reserved. + * + * This code may be used to extend the functionality of Burp Suite Community Edition + * and Burp Suite Professional, provided that this usage does not violate the + * license terms for those products. + */ + +package burp.api.montoya.http.message; + +import burp.api.montoya.core.Annotations; +import burp.api.montoya.core.Marker; +import burp.api.montoya.http.HttpService; +import burp.api.montoya.http.handler.TimingData; +import burp.api.montoya.http.message.requests.HttpRequest; +import burp.api.montoya.http.message.requests.MalformedRequestException; +import burp.api.montoya.http.message.responses.HttpResponse; + +import java.util.List; +import java.util.Optional; +import java.util.regex.Pattern; + +import static burp.api.montoya.internal.ObjectFactoryLocator.FACTORY; + +/** + * This interface is used to define a coupling between {@link HttpRequest} and {@link HttpResponse}. + */ +public interface HttpRequestResponse +{ + /** + * @return The HTTP request message. + */ + HttpRequest request(); + + /** + * @return The HTTP response message. + */ + HttpResponse response(); + + /** + * HTTP service for the request. + * + * @return An {@link HttpService} object containing details of the HTTP service. + */ + HttpService httpService(); + + /** + * @return The annotations. + */ + Annotations annotations(); + + /** + * Retrieve the timing data associated with this request if available. + * + * @return The timing data. + */ + Optional timingData(); + + /** + * Retrieve the URL for the request.
+ * If the request is malformed, then a {@link MalformedRequestException} is thrown. + * + * @return The URL in the request. + * @throws MalformedRequestException if request is malformed. + * @deprecated use {@link #request()} url instead. + */ + @Deprecated(forRemoval = true) + String url(); + + /** + * @return True if there is an HTTP response message. + */ + boolean hasResponse(); + + /** + * @return The detected content type of the request. + * @deprecated use {@link #request()} contentType instead. + */ + @Deprecated(forRemoval = true) + ContentType contentType(); + + /** + * HTTP status code contained in the response. + * + * @return HTTP status code or -1 if there is no response. + * @deprecated use {@link #response()} statusCode instead. + */ + @Deprecated(forRemoval = true) + short statusCode(); + + /** + * @return List of request markers + */ + List requestMarkers(); + + /** + * @return List of response markers + */ + List responseMarkers(); + + /** + * Searches the data in the HTTP request, response and notes for the specified search term. + * + * @param searchTerm The value to be searched for. + * @param caseSensitive Flags whether the search is case-sensitive. + * + * @return True if the search term is found. + */ + boolean contains(String searchTerm, boolean caseSensitive); + + /** + * Searches the data in the HTTP request, response and notes for the specified regular expression. + * + * @param pattern The regular expression to be searched for. + * + * @return True if the pattern is matched. + */ + boolean contains(Pattern pattern); + + /** + * Create a copy of the {@code HttpRequestResponse} in temporary file.
+ * This method is used to save the {@code HttpRequestResponse} object to a temporary file, + * so that it is no longer held in memory. Extensions can use this method to convert + * {@code HttpRequest} objects into a form suitable for long-term usage. + * + * @return A new {@code ByteArray} instance stored in temporary file. + */ + HttpRequestResponse copyToTempFile(); + + /** + * Create a copy of the {@code HttpRequestResponse} with the added annotations. + * + * @param annotations annotations to add. + * + * @return A new {@code HttpRequestResponse} instance. + */ + HttpRequestResponse withAnnotations(Annotations annotations); + + /** + * Create a copy of the {@code HttpRequestResponse} with the added request markers. + * + * @param requestMarkers Request markers to add. + * + * @return A new {@code HttpRequestResponse} instance. + */ + HttpRequestResponse withRequestMarkers(List requestMarkers); + + /** + * Create a copy of the {@code HttpRequestResponse} with the added request markers. + * + * @param requestMarkers Request markers to add. + * + * @return A new {@code HttpRequestResponse} instance. + */ + HttpRequestResponse withRequestMarkers(Marker... requestMarkers); + + /** + * Create a copy of the {@code HttpRequestResponse} with the added response markers. + * + * @param responseMarkers Response markers to add. + * + * @return A new {@code HttpRequestResponse} instance. + */ + HttpRequestResponse withResponseMarkers(List responseMarkers); + + /** + * Create a copy of the {@code HttpRequestResponse} with the added response markers. + * + * @param responseMarkers Response markers to add. + * + * @return A new {@code HttpRequestResponse} instance. + */ + HttpRequestResponse withResponseMarkers(Marker... responseMarkers); + + /** + * Create a new instance of {@link HttpRequestResponse}.
+ * + * @param request The HTTP request. + * @param response The HTTP response. + * + * @return A new {@link HttpRequestResponse} instance. + */ + static HttpRequestResponse httpRequestResponse(HttpRequest request, HttpResponse response) + { + return FACTORY.httpRequestResponse(request, response); + } + + /** + * Create a new instance of {@link HttpRequestResponse}.
+ * + * @param httpRequest The HTTP request. + * @param httpResponse The HTTP response. + * @param annotations annotations. + * + * @return A new {@link HttpRequestResponse} instance. + */ + static HttpRequestResponse httpRequestResponse(HttpRequest httpRequest, HttpResponse httpResponse, Annotations annotations) + { + return FACTORY.httpRequestResponse(httpRequest, httpResponse, annotations); + } +} diff --git a/src/burp/api/montoya/http/message/MimeType.java b/src/burp/api/montoya/http/message/MimeType.java new file mode 100644 index 0000000..20de4c3 --- /dev/null +++ b/src/burp/api/montoya/http/message/MimeType.java @@ -0,0 +1,56 @@ +/* + * Copyright (c) 2022-2023. PortSwigger Ltd. All rights reserved. + * + * This code may be used to extend the functionality of Burp Suite Community Edition + * and Burp Suite Professional, provided that this usage does not violate the + * license terms for those products. + */ + +package burp.api.montoya.http.message; + +/** + * MIME types that are recognised by Burp. + */ +public enum MimeType +{ + NONE("none"), + UNRECOGNIZED("unrecognized content"), + AMBIGUOUS("ambiguous"), + HTML("HTML"), + PLAIN_TEXT("plain text"), + CSS("CSS"), + SCRIPT("script"), + JSON("JSON"), + RTF("RTF"), + XML("XML"), + YAML("YAML"), + IMAGE_UNKNOWN("an unknown image type"), + IMAGE_JPEG("a JPEG image"), + IMAGE_GIF("a GIF image"), + IMAGE_PNG("a PNG image"), + IMAGE_BMP("a BMP image"), + IMAGE_TIFF("a TIFF image"), + IMAGE_SVG_XML("a SVG image"), + SOUND("sound"), + VIDEO("video"), + APPLICATION_FLASH("a flash object"), + APPLICATION_UNKNOWN("an unknown application type"), + FONT_WOFF("a WOFF font file"), + FONT_WOFF2("a WOFF2 font file"), + LEGACY_SER_AMF(""); + + private final String description; + + MimeType(String description) + { + this.description = description; + } + + /** + * @return MIME type description. + */ + public String description() + { + return description; + } +} diff --git a/src/burp/api/montoya/http/message/StatusCodeClass.java b/src/burp/api/montoya/http/message/StatusCodeClass.java new file mode 100644 index 0000000..d2dccf3 --- /dev/null +++ b/src/burp/api/montoya/http/message/StatusCodeClass.java @@ -0,0 +1,63 @@ +package burp.api.montoya.http.message; + +/** + * Status code classes that are defined in the HTTP standard. + */ +public enum StatusCodeClass +{ + /** + * Informational response (100 to 199). + */ + CLASS_1XX_INFORMATIONAL_RESPONSE(100, 200), + /** + * Success (200 to 299). + */ + CLASS_2XX_SUCCESS(200, 300), + /** + * Redirection (300 to 399). + */ + CLASS_3XX_REDIRECTION(300, 400), + /** + * Client errors (400 to 499). + */ + CLASS_4XX_CLIENT_ERRORS(400, 500), + /** + * Server errors (500 to 599). + */ + CLASS_5XX_SERVER_ERRORS(500, 600); + + private final int startStatusCodeInclusive; + private final int endStatusCodeExclusive; + + StatusCodeClass(int startStatusCodeInclusive, int endStatusCodeExclusive) + { + this.startStatusCodeInclusive = startStatusCodeInclusive; + this.endStatusCodeExclusive = endStatusCodeExclusive; + } + + /** + * @return the inclusive start status code. + */ + public int startStatusCodeInclusive() + { + return startStatusCodeInclusive; + } + + /** + * @return the exclusive end status code. + */ + public int endStatusCodeExclusive() + { + return endStatusCodeExclusive; + } + + /** + * @param statusCode The status code to test. + * + * @return True if the status code is in the status code class. + */ + public boolean contains(int statusCode) + { + return startStatusCodeInclusive <= statusCode && statusCode < endStatusCodeExclusive; + } +} diff --git a/src/burp/api/montoya/http/message/params/HttpParameter.java b/src/burp/api/montoya/http/message/params/HttpParameter.java new file mode 100644 index 0000000..602aad4 --- /dev/null +++ b/src/burp/api/montoya/http/message/params/HttpParameter.java @@ -0,0 +1,85 @@ +/* + * Copyright (c) 2022-2023. PortSwigger Ltd. All rights reserved. + * + * This code may be used to extend the functionality of Burp Suite Community Edition + * and Burp Suite Professional, provided that this usage does not violate the + * license terms for those products. + */ + +package burp.api.montoya.http.message.params; + +import static burp.api.montoya.internal.ObjectFactoryLocator.FACTORY; + +/** + * Burp HTTP parameter able to retrieve to hold details about an HTTP request parameter. + */ +public interface HttpParameter +{ + /** + * @return The parameter type. + */ + HttpParameterType type(); + + /** + * @return The parameter name. + */ + String name(); + + /** + * @return The parameter value. + */ + String value(); + + /** + * Create a new Instance of {@code HttpParameter} with {@link HttpParameterType#URL} type. + * + * @param name The parameter name. + * @param value The parameter value. + * + * @return A new {@code HttpParameter} instance. + */ + static HttpParameter urlParameter(String name, String value) + { + return FACTORY.urlParameter(name, value); + } + + /** + * Create a new Instance of {@code HttpParameter} with {@link HttpParameterType#BODY} type. + * + * @param name The parameter name. + * @param value The parameter value. + * + * @return A new {@code HttpParameter} instance. + */ + static HttpParameter bodyParameter(String name, String value) + { + return FACTORY.bodyParameter(name, value); + } + + /** + * Create a new Instance of {@code HttpParameter} with {@link HttpParameterType#COOKIE} type. + * + * @param name The parameter name. + * @param value The parameter value. + * + * @return A new {@code HttpParameter} instance. + */ + static HttpParameter cookieParameter(String name, String value) + { + return FACTORY.cookieParameter(name, value); + } + + /** + * Create a new Instance of {@code HttpParameter} with the specified type. + * + * @param name The parameter name. + * @param value The parameter value. + * @param type The header type. + * + * @return A new {@code HttpParameter} instance. + */ + static HttpParameter parameter(String name, String value, HttpParameterType type) + { + return FACTORY.parameter(name, value, type); + } +} diff --git a/src/burp/api/montoya/http/message/params/HttpParameterType.java b/src/burp/api/montoya/http/message/params/HttpParameterType.java new file mode 100644 index 0000000..9cd0a1f --- /dev/null +++ b/src/burp/api/montoya/http/message/params/HttpParameterType.java @@ -0,0 +1,23 @@ +/* + * Copyright (c) 2022-2023. PortSwigger Ltd. All rights reserved. + * + * This code may be used to extend the functionality of Burp Suite Community Edition + * and Burp Suite Professional, provided that this usage does not violate the + * license terms for those products. + */ + +package burp.api.montoya.http.message.params; + +/** + * HTTP parameter types. + */ +public enum HttpParameterType +{ + URL, + BODY, + COOKIE, + XML, + XML_ATTRIBUTE, + MULTIPART_ATTRIBUTE, + JSON +} diff --git a/src/burp/api/montoya/http/message/params/ParsedHttpParameter.java b/src/burp/api/montoya/http/message/params/ParsedHttpParameter.java new file mode 100644 index 0000000..91acc37 --- /dev/null +++ b/src/burp/api/montoya/http/message/params/ParsedHttpParameter.java @@ -0,0 +1,49 @@ +/* + * Copyright (c) 2022-2023. PortSwigger Ltd. All rights reserved. + * + * This code may be used to extend the functionality of Burp Suite Community Edition + * and Burp Suite Professional, provided that this usage does not violate the + * license terms for those products. + */ + +package burp.api.montoya.http.message.params; + +import burp.api.montoya.core.Range; + +/** + * Burp {@link HttpParameter} with additional details about an HTTP request parameter that has been parsed by Burp. + */ +public interface ParsedHttpParameter extends HttpParameter +{ + /** + * @return The parameter type. + */ + @Override + HttpParameterType type(); + + /** + * @return The parameter name. + */ + @Override + String name(); + + /** + * @return The parameter value. + */ + @Override + String value(); + + /** + * Offsets of the parameter name within the HTTP request. + * + * @return The parameter name offsets. + */ + Range nameOffsets(); + + /** + * Offsets of the parameter value within the HTTP request. + * + * @return The parameter value offsets. + */ + Range valueOffsets(); +} diff --git a/src/burp/api/montoya/http/message/requests/HttpRequest.java b/src/burp/api/montoya/http/message/requests/HttpRequest.java new file mode 100644 index 0000000..261f8d3 --- /dev/null +++ b/src/burp/api/montoya/http/message/requests/HttpRequest.java @@ -0,0 +1,616 @@ +/* + * Copyright (c) 2022-2023. PortSwigger Ltd. All rights reserved. + * + * This code may be used to extend the functionality of Burp Suite Community Edition + * and Burp Suite Professional, provided that this usage does not violate the + * license terms for those products. + */ + +package burp.api.montoya.http.message.requests; + +import burp.api.montoya.core.ByteArray; +import burp.api.montoya.core.Marker; +import burp.api.montoya.http.HttpService; +import burp.api.montoya.http.message.ContentType; +import burp.api.montoya.http.message.HttpHeader; +import burp.api.montoya.http.message.HttpMessage; +import burp.api.montoya.http.message.params.HttpParameter; +import burp.api.montoya.http.message.params.HttpParameterType; +import burp.api.montoya.http.message.params.ParsedHttpParameter; + +import java.util.List; +import java.util.regex.Pattern; + +import static burp.api.montoya.internal.ObjectFactoryLocator.FACTORY; + +/** + * Burp HTTP request able to retrieve and modify details of an HTTP request. + */ +public interface HttpRequest extends HttpMessage +{ + /** + * @return True if the request is in-scope. + */ + boolean isInScope(); + + /** + * HTTP service for the request. + * + * @return An {@link HttpService} object containing details of the HTTP service. + */ + HttpService httpService(); + + /** + * URL for the request. + * If the request is malformed, then a {@link MalformedRequestException} is thrown. + * + * @return The URL in the request. + * @throws MalformedRequestException if request is malformed. + */ + String url(); + + /** + * HTTP method for the request. + * If the request is malformed, then a {@link MalformedRequestException} is thrown. + * + * @return The HTTP method used in the request. + * @throws MalformedRequestException if request is malformed. + */ + String method(); + + /** + * Request path including the query parameters. + * If the request is malformed, then a {@link MalformedRequestException} is thrown. + * + * @return the path and query parameters. + * @throws MalformedRequestException if request is malformed. + */ + String path(); + + /** + * The query for the request. + * If the request is malformed, then a {@link MalformedRequestException} is thrown. + * + * @return the query, or an empty string if there is none. + * @throws MalformedRequestException if request is malformed. + */ + String query(); + + /** + * Request path excluding the query parameters. + * If the request is malformed, then a {@link MalformedRequestException} is thrown. + * + * @return the path excluding query parameters. + * @throws MalformedRequestException if request is malformed. + */ + String pathWithoutQuery(); + + /** + * The file extension for the request. + * If the request is malformed, then a {@link MalformedRequestException} is thrown. + * + * @return the file extension, or an empty string if there is none. + * @throws MalformedRequestException if request is malformed. + */ + String fileExtension(); + + /** + * @return The detected content type of the request. + */ + ContentType contentType(); + + /** + * @return The parameters contained in the request. + */ + List parameters(); + + /** + * @param type The type of parameter that will be returned in the filtered list. + * + * @return A filtered list of {@link ParsedHttpParameter} containing only the provided type. + */ + List parameters(HttpParameterType type); + + /** + * @return True if the request has parameters. + */ + boolean hasParameters(); + + /** + * @return True if the request has parameters of type {@link HttpParameterType} + */ + boolean hasParameters(HttpParameterType type); + + /** + * @param name The name of the parameter to find. + * @param type The type of the parameter to find. + * + * @return An instance of {@link ParsedHttpParameter} that matches the type and name specified. {@code null} if not found. + */ + ParsedHttpParameter parameter(String name, HttpParameterType type); + + /** + * @param name The name of the parameter to get the value from. + * @param type The type of the parameter to get the value from. + * + * @return The value of the parameter that matches the name and type specified. {@code null} if not found. + */ + String parameterValue(String name, HttpParameterType type); + + /** + * @param name The name of the parameter to find. + * @param type The type of the parameter to find. + * + * @return {@code true} if a parameter exists that matches the name and type specified. {@code false} if not found. + */ + boolean hasParameter(String name, HttpParameterType type); + + /** + * @param parameter An instance of {@link HttpParameter} to match to an existing parameter. + * + * @return {@code true} if a parameter exists that matches the data within the provided {@link HttpParameter}. {@code false} if not found. + */ + boolean hasParameter(HttpParameter parameter); + + /** + * @param header The header to check if it exists in the request. + * + * @return True if the header exists in the request. + */ + @Override + boolean hasHeader(HttpHeader header); + + /** + * @param name The name of the header to query within the request. + * + * @return True if a header exists in the request with the supplied name. + */ + @Override + boolean hasHeader(String name); + + /** + * @param name The name of the header to check. + * @param value The value of the header to check. + * + * @return True if a header exists in the request that matches the name and value supplied. + */ + @Override + boolean hasHeader(String name, String value); + + /** + * @param name The name of the header to retrieve. + * + * @return An instance of {@link HttpHeader} that matches the name supplied, {@code null} if no match found. + */ + @Override + HttpHeader header(String name); + + /** + * @param name The name of the header to retrieve. + * + * @return The {@code String} value of the header that matches the name supplied, {@code null} if no match found. + */ + @Override + String headerValue(String name); + + /** + * HTTP headers contained in the message. + * + * @return A list of HTTP headers. + */ + @Override + List headers(); + + /** + * HTTP Version text parsed from the request or response line for HTTP 1 messages. + * HTTP 2 messages will return "HTTP/2" + * + * @return Version string + */ + @Override + String httpVersion(); + + /** + * Offset within the message where the message body begins. + * + * @return The message body offset. + */ + @Override + int bodyOffset(); + + /** + * Body of a message as a byte array. + * + * @return The body of a message as a byte array. + */ + @Override + ByteArray body(); + + /** + * Body of a message as a {@code String}. + * + * @return The body of a message as a {@code String}. + */ + @Override + String bodyToString(); + + /** + * Markers for the message. + * + * @return A list of markers. + */ + @Override + List markers(); + + /** + * Searches the data in the HTTP message for the specified search term. + * + * @param searchTerm The value to be searched for. + * @param caseSensitive Flags whether the search is case-sensitive. + * + * @return True if the search term is found. + */ + @Override + boolean contains(String searchTerm, boolean caseSensitive); + + /** + * Searches the data in the HTTP message for the specified regular expression. + * + * @param pattern The regular expression to be searched for. + * + * @return True if the pattern is matched. + */ + @Override + boolean contains(Pattern pattern); + + /** + * Message as a byte array. + * + * @return The message as a byte array. + */ + @Override + ByteArray toByteArray(); + + /** + * Message as a {@code String}. + * + * @return The message as a {@code String}. + */ + @Override + String toString(); + + /** + * Create a copy of the {@code HttpRequest} in temporary file.
+ * This method is used to save the {@code HttpRequest} object to a temporary file, + * so that it is no longer held in memory. Extensions can use this method to convert + * {@code HttpRequest} objects into a form suitable for long-term usage. + * + * @return A new {@code HttpRequest} instance stored in temporary file. + */ + HttpRequest copyToTempFile(); + + /** + * Create a copy of the {@code HttpRequest} with the new service. + * + * @param service An {@link HttpService} reference to add. + * + * @return A new {@code HttpRequest} instance. + */ + HttpRequest withService(HttpService service); + + /** + * Create a copy of the {@code HttpRequest} with the new path. + * + * @param path The path to use. + * + * @return A new {@code HttpRequest} instance with updated path. + */ + HttpRequest withPath(String path); + + /** + * Create a copy of the {@code HttpRequest} with the new method. + * + * @param method the method to use + * + * @return a new {@code HttpRequest} instance with updated method. + */ + HttpRequest withMethod(String method); + + /** + * Create a copy of the {@code HttpRequest} with the added or updated header.
+ * If the header exists in the request, it is updated.
+ * If the header doesn't exist in the request, it is added. + * + * @param header HTTP header to add or update. + * + * @return A new {@code HttpRequest} with the added or updated header. + */ + HttpRequest withHeader(HttpHeader header); + + /** + * Create a copy of the {@code HttpRequest} with the added or updated header.
+ * If the header exists in the request, it is updated.
+ * If the header doesn't exist in the request, it is added. + * + * @param name The name of the header. + * @param value The value of the header. + * + * @return A new {@code HttpRequest} with the added or updated header. + */ + HttpRequest withHeader(String name, String value); + + /** + * Create a copy of the {@code HttpRequest} with the HTTP parameter.
+ * If the parameter exists in the request, it is updated.
+ * If the parameter doesn't exist in the request, it is added. + * + * @param parameters HTTP parameter to add or update. + * + * @return A new {@code HttpRequest} with the added or updated parameter. + */ + HttpRequest withParameter(HttpParameter parameters); + + /** + * Create a copy of the {@code HttpRequest} with the added HTTP parameters. + * + * @param parameters HTTP parameters to add. + * + * @return A new {@code HttpRequest} instance. + */ + HttpRequest withAddedParameters(List parameters); + + /** + * Create a copy of the {@code HttpRequest} with the added HTTP parameters. + * + * @param parameters HTTP parameters to add. + * + * @return A new {@code HttpRequest} instance. + */ + HttpRequest withAddedParameters(HttpParameter... parameters); + + /** + * Create a copy of the {@code HttpRequest} with the removed HTTP parameters. + * + * @param parameters HTTP parameters to remove. + * + * @return A new {@code HttpRequest} instance. + */ + HttpRequest withRemovedParameters(List parameters); + + /** + * Create a copy of the {@code HttpRequest} with the removed HTTP parameters. + * + * @param parameters HTTP parameters to remove. + * + * @return A new {@code HttpRequest} instance. + */ + HttpRequest withRemovedParameters(HttpParameter... parameters); + + /** + * Create a copy of the {@code HttpRequest} with the updated HTTP parameters.
+ * + * @param parameters HTTP parameters to update. + * + * @return A new {@code HttpRequest} instance. + */ + HttpRequest withUpdatedParameters(List parameters); + + /** + * Create a copy of the {@code HttpRequest} with the updated HTTP parameters.
+ * + * @param parameters HTTP parameters to update. + * + * @return A new {@code HttpRequest} instance. + */ + HttpRequest withUpdatedParameters(HttpParameter... parameters); + + /** + * Create a copy of the {@code HttpRequest} with the transformation applied. + * + * @param transformation Transformation to apply. + * + * @return A new {@code HttpRequest} instance. + */ + HttpRequest withTransformationApplied(HttpTransformation transformation); + + /** + * Create a copy of the {@code HttpRequest} with the updated body.
+ * Updates Content-Length header. + * + * @param body the new body for the request + * + * @return A new {@code HttpRequest} instance. + */ + HttpRequest withBody(String body); + + /** + * Create a copy of the {@code HttpRequest} with the updated body.
+ * Updates Content-Length header. + * + * @param body the new body for the request + * + * @return A new {@code HttpRequest} instance. + */ + HttpRequest withBody(ByteArray body); + + /** + * Create a copy of the {@code HttpRequest} with the added header. + * + * @param name The name of the header. + * @param value The value of the header. + * + * @return The updated HTTP request with the added header. + */ + HttpRequest withAddedHeader(String name, String value); + + /** + * Create a copy of the {@code HttpRequest} with the added header. + * + * @param header The {@link HttpHeader} to add to the HTTP request. + * + * @return The updated HTTP request with the added header. + */ + HttpRequest withAddedHeader(HttpHeader header); + + /** + * Create a copy of the {@code HttpRequest} with the updated header. + * + * @param name The name of the header to update the value of. + * @param value The new value of the specified HTTP header. + * + * @return The updated request containing the updated header. + */ + HttpRequest withUpdatedHeader(String name, String value); + + /** + * Create a copy of the {@code HttpRequest} with the updated header. + * + * @param header The {@link HttpHeader} to update containing the new value. + * + * @return The updated request containing the updated header. + */ + HttpRequest withUpdatedHeader(HttpHeader header); + + /** + * Removes an existing HTTP header from the current request. + * + * @param name The name of the HTTP header to remove from the request. + * + * @return The updated request containing the removed header. + */ + HttpRequest withRemovedHeader(String name); + + /** + * Removes an existing HTTP header from the current request. + * + * @param header The {@link HttpHeader} to remove from the request. + * + * @return The updated request containing the removed header. + */ + HttpRequest withRemovedHeader(HttpHeader header); + + /** + * Create a copy of the {@code HttpRequest} with the added markers. + * + * @param markers Request markers to add. + * + * @return A new {@link HttpRequest} instance. + */ + HttpRequest withMarkers(List markers); + + /** + * Create a copy of the {@code HttpRequest} with the added markers. + * + * @param markers Request markers to add. + * + * @return A new {@link HttpRequest} instance. + */ + HttpRequest withMarkers(Marker... markers); + + /** + * Create a copy of the {@code HttpRequest} with added default headers. + * + * @return a new {@link HttpRequest} with added default headers + */ + HttpRequest withDefaultHeaders(); + + /** + * Create a new empty instance of {@link HttpRequest}.
+ * + * @return A new {@link HttpRequest} instance. + */ + static HttpRequest httpRequest() + { + return FACTORY.httpRequest(); + } + + /** + * Create a new instance of {@link HttpRequest}.
+ * + * @param request The HTTP request + * + * @return A new {@link HttpRequest} instance. + */ + static HttpRequest httpRequest(ByteArray request) + { + return FACTORY.httpRequest(request); + } + + /** + * Create a new instance of {@link HttpRequest}.
+ * + * @param request The HTTP request. + * + * @return A new {@link HttpRequest} instance. + */ + static HttpRequest httpRequest(String request) + { + return FACTORY.httpRequest(request); + } + + /** + * Create a new instance of {@link HttpRequest}.
+ * + * @param service An HTTP service for the request. + * @param request The HTTP request. + * + * @return A new {@link HttpRequest} instance. + */ + static HttpRequest httpRequest(HttpService service, ByteArray request) + { + return FACTORY.httpRequest(service, request); + } + + /** + * Create a new instance of {@link HttpRequest}.
+ * + * @param service An HTTP service for the request. + * @param request The HTTP request. + * + * @return A new {@link HttpRequest} instance. + */ + static HttpRequest httpRequest(HttpService service, String request) + { + return FACTORY.httpRequest(service, request); + } + + /** + * Create a new instance of {@link HttpRequest}.
+ * + * @param url A URL for the request. + * + * @return A new {@link HttpRequest} instance. + */ + static HttpRequest httpRequestFromUrl(String url) + { + return FACTORY.httpRequestFromUrl(url); + } + + /** + * Create a new instance of {@link HttpRequest} containing HTTP 2 headers and body.
+ * + * @param service An HTTP service for the request. + * @param headers A list of HTTP 2 headers. + * @param body A body of the HTTP 2 request. + * + * @return A new {@link HttpRequest} instance. + */ + static HttpRequest http2Request(HttpService service, List headers, ByteArray body) + { + return FACTORY.http2Request(service, headers, body); + } + + /** + * Create a new instance of {@link HttpRequest} containing HTTP 2 headers and body.
+ * + * @param service An HTTP service for the request. + * @param headers A list of HTTP 2 headers. + * @param body A body of the HTTP 2 request. + * + * @return A new {@link HttpRequest} instance. + */ + static HttpRequest http2Request(HttpService service, List headers, String body) + { + return FACTORY.http2Request(service, headers, body); + } +} diff --git a/src/burp/api/montoya/http/message/requests/HttpTransformation.java b/src/burp/api/montoya/http/message/requests/HttpTransformation.java new file mode 100644 index 0000000..085054b --- /dev/null +++ b/src/burp/api/montoya/http/message/requests/HttpTransformation.java @@ -0,0 +1,22 @@ +/* + * Copyright (c) 2022-2023. PortSwigger Ltd. All rights reserved. + * + * This code may be used to extend the functionality of Burp Suite Community Edition + * and Burp Suite Professional, provided that this usage does not violate the + * license terms for those products. + */ + +package burp.api.montoya.http.message.requests; + +/** + * This enum defines transformations that Burp can apply to an HTTP request. + */ +public enum HttpTransformation +{ + /** + * Convert a GET request into a POST request
+ * or
+ * Convert a POST request into a GET request
+ */ + TOGGLE_METHOD +} diff --git a/src/burp/api/montoya/http/message/requests/MalformedRequestException.java b/src/burp/api/montoya/http/message/requests/MalformedRequestException.java new file mode 100644 index 0000000..f09f10d --- /dev/null +++ b/src/burp/api/montoya/http/message/requests/MalformedRequestException.java @@ -0,0 +1,20 @@ +/* + * Copyright (c) 2022-2023. PortSwigger Ltd. All rights reserved. + * + * This code may be used to extend the functionality of Burp Suite Community Edition + * and Burp Suite Professional, provided that this usage does not violate the + * license terms for those products. + */ + +package burp.api.montoya.http.message.requests; + +/** + * This class represents an exception which is thrown when trying to retrieve attributes from a malformed request. + */ +public class MalformedRequestException extends RuntimeException +{ + public MalformedRequestException(String message) + { + super(message); + } +} diff --git a/src/burp/api/montoya/http/message/responses/HttpResponse.java b/src/burp/api/montoya/http/message/responses/HttpResponse.java new file mode 100644 index 0000000..0debfef --- /dev/null +++ b/src/burp/api/montoya/http/message/responses/HttpResponse.java @@ -0,0 +1,421 @@ +/* + * Copyright (c) 2022-2023. PortSwigger Ltd. All rights reserved. + * + * This code may be used to extend the functionality of Burp Suite Community Edition + * and Burp Suite Professional, provided that this usage does not violate the + * license terms for those products. + */ + +package burp.api.montoya.http.message.responses; + +import burp.api.montoya.core.ByteArray; +import burp.api.montoya.core.Marker; +import burp.api.montoya.http.message.Cookie; +import burp.api.montoya.http.message.HttpHeader; +import burp.api.montoya.http.message.HttpMessage; +import burp.api.montoya.http.message.MimeType; +import burp.api.montoya.http.message.StatusCodeClass; +import burp.api.montoya.http.message.responses.analysis.Attribute; +import burp.api.montoya.http.message.responses.analysis.AttributeType; +import burp.api.montoya.http.message.responses.analysis.KeywordCount; + +import java.util.List; +import java.util.regex.Pattern; + +import static burp.api.montoya.internal.ObjectFactoryLocator.FACTORY; + +/** + * Burp HTTP response able to retrieve and modify details about an HTTP response. + */ +public interface HttpResponse extends HttpMessage +{ + /** + * Obtain the HTTP status code contained in the response. + * + * @return HTTP status code. + */ + short statusCode(); + + /** + * Obtain the HTTP reason phrase contained in the response for HTTP 1 messages. + * HTTP 2 messages will return a mapped phrase based on the status code. + * + * @return HTTP Reason phrase. + */ + String reasonPhrase(); + + /** + * Test whether the status code is in the specified class. + * + * @param statusCodeClass The class of status code to test. + * + * @return True if the status code is in the class. + */ + boolean isStatusCodeClass(StatusCodeClass statusCodeClass); + + /** + * Obtain details of the HTTP cookies set in the response. + * + * @return A list of {@link Cookie} objects representing the cookies set in the response, if any. + */ + List cookies(); + + /** + * @param name The name of the cookie to find. + * + * @return An instance of {@link Cookie} that matches the name provided. {@code null} if not found. + */ + Cookie cookie(String name); + + /** + * @param name The name of the cookie to retrieve the value from. + * + * @return The value of the cookie that matches the name provided. {@code null} if not found. + */ + String cookieValue(String name); + + /** + * @param name The name of the cookie to check if it exists in the response. + * + * @return {@code true} If a cookie exists within the response that matches the name provided. {@code false} if not. + */ + boolean hasCookie(String name); + + /** + * @param cookie An instance of {@link Cookie} to check if it exists in the response. + * + * @return {@code true} If a cookie exists within the response that matches the {@link Cookie} provided. {@code false} if not. + */ + boolean hasCookie(Cookie cookie); + + /** + * Obtain the MIME type of the response, as determined by Burp Suite. + * + * @return The MIME type. + */ + MimeType mimeType(); + + /** + * Obtain the MIME type of the response, as stated in the HTTP headers. + * + * @return The stated MIME type. + */ + MimeType statedMimeType(); + + /** + * Obtain the MIME type of the response, as inferred from the contents of the HTTP message body. + * + * @return The inferred MIME type. + */ + MimeType inferredMimeType(); + + /** + * Retrieve the number of types given keywords appear in the response. + * + * @param keywords Keywords to count. + * + * @return List of keyword counts in the order they were provided. + */ + List keywordCounts(String... keywords); + + /** + * Retrieve the values of response attributes. + * + * @param types Response attributes to retrieve values for. + * + * @return List of {@link Attribute} objects. + */ + List attributes(AttributeType... types); + + /** + * @param header The header to check if it exists in the request. + * + * @return True if the header exists in the request. + */ + @Override + boolean hasHeader(HttpHeader header); + + /** + * @param name The name of the header to query within the request. + * + * @return True if a header exists in the request with the supplied name. + */ + @Override + boolean hasHeader(String name); + + /** + * @param name The name of the header to check. + * @param value The value of the header to check. + * + * @return True if a header exists in the request that matches the name and value supplied. + */ + @Override + boolean hasHeader(String name, String value); + + /** + * @param name The name of the header to retrieve. + * + * @return An instance of {@link HttpHeader} that matches the name supplied, {@code null} if no match found. + */ + @Override + HttpHeader header(String name); + + /** + * @param name The name of the header to retrieve. + * + * @return The {@code String} value of the header that matches the name supplied, {@code null} if no match found. + */ + @Override + String headerValue(String name); + + /** + * HTTP headers contained in the message. + * + * @return A list of HTTP headers. + */ + @Override + List headers(); + + /** + * HTTP Version text parsed from the request or response line for HTTP 1 messages. + * HTTP 2 messages will return "HTTP/2" + * + * @return Version string + */ + @Override + String httpVersion(); + + /** + * Offset within the message where the message body begins. + * + * @return The message body offset. + */ + @Override + int bodyOffset(); + + /** + * Body of a message as a byte array. + * + * @return The body of a message as a byte array. + */ + @Override + ByteArray body(); + + /** + * Body of a message as a {@code String}. + * + * @return The body of a message as a {@code String}. + */ + @Override + String bodyToString(); + + /** + * Markers for the message. + * + * @return A list of markers. + */ + @Override + List markers(); + + /** + * Searches the data in the HTTP message for the specified search term. + * + * @param searchTerm The value to be searched for. + * @param caseSensitive Flags whether the search is case-sensitive. + * + * @return True if the search term is found. + */ + @Override + boolean contains(String searchTerm, boolean caseSensitive); + + /** + * Searches the data in the HTTP message for the specified regular expression. + * + * @param pattern The regular expression to be searched for. + * + * @return True if the pattern is matched. + */ + @Override + boolean contains(Pattern pattern); + + /** + * Message as a byte array. + * + * @return The message as a byte array. + */ + @Override + ByteArray toByteArray(); + + /** + * Message as a {@code String}. + * + * @return The message as a {@code String}. + */ + @Override + String toString(); + + /** + * Create a copy of the {@code HttpResponse} in temporary file.
+ * This method is used to save the {@code HttpResponse} object to a temporary file, + * so that it is no longer held in memory. Extensions can use this method to convert + * {@code HttpResponse} objects into a form suitable for long-term usage. + * + * @return A new {@code HttpResponse} instance stored in temporary file. + */ + HttpResponse copyToTempFile(); + + /** + * Create a copy of the {@code HttpResponse} with the provided status code. + * + * @param statusCode the new status code for response + * + * @return A new {@code HttpResponse} instance. + */ + HttpResponse withStatusCode(short statusCode); + + /** + * Create a copy of the {@code HttpResponse} with the new reason phrase. + * + * @param reasonPhrase the new reason phrase for response + * + * @return A new {@code HttpResponse} instance. + */ + HttpResponse withReasonPhrase(String reasonPhrase); + + /** + * Create a copy of the {@code HttpResponse} with the new http version. + * + * @param httpVersion the new http version for response + * + * @return A new {@code HttpResponse} instance. + */ + HttpResponse withHttpVersion(String httpVersion); + + /** + * Create a copy of the {@code HttpResponse} with the updated body.
+ * Updates Content-Length header. + * + * @param body the new body for the response + * + * @return A new {@code HttpResponse} instance. + */ + HttpResponse withBody(String body); + + /** + * Create a copy of the {@code HttpResponse} with the updated body.
+ * Updates Content-Length header. + * + * @param body the new body for the response + * + * @return A new {@code HttpResponse} instance. + */ + HttpResponse withBody(ByteArray body); + + /** + * Create a copy of the {@code HttpResponse} with the added header. + * + * @param header The {@link HttpHeader} to add to the response. + * + * @return The updated response containing the added header. + */ + HttpResponse withAddedHeader(HttpHeader header); + + /** + * Create a copy of the {@code HttpResponse} with the added header. + * + * @param name The name of the header. + * @param value The value of the header. + * + * @return The updated response containing the added header. + */ + HttpResponse withAddedHeader(String name, String value); + + /** + * Create a copy of the {@code HttpResponse} with the updated header. + * + * @param header The {@link HttpHeader} to update containing the new value. + * + * @return The updated response containing the updated header. + */ + HttpResponse withUpdatedHeader(HttpHeader header); + + /** + * Create a copy of the {@code HttpResponse} with the updated header. + * + * @param name The name of the header to update the value of. + * @param value The new value of the specified HTTP header. + * + * @return The updated response containing the updated header. + */ + HttpResponse withUpdatedHeader(String name, String value); + + /** + * Create a copy of the {@code HttpResponse} with the removed header. + * + * @param header The {@link HttpHeader} to remove from the response. + * + * @return The updated response containing the removed header. + */ + HttpResponse withRemovedHeader(HttpHeader header); + + /** + * Create a copy of the {@code HttpResponse} with the removed header. + * + * @param name The name of the HTTP header to remove from the response. + * + * @return The updated response containing the removed header. + */ + HttpResponse withRemovedHeader(String name); + + /** + * Create a copy of the {@code HttpResponse} with the added markers. + * + * @param markers Request markers to add. + * + * @return A new {@code MarkedHttpRequestResponse} instance. + */ + HttpResponse withMarkers(List markers); + + /** + * Create a copy of the {@code HttpResponse} with the added markers. + * + * @param markers Request markers to add. + * + * @return A new {@code MarkedHttpRequestResponse} instance. + */ + HttpResponse withMarkers(Marker... markers); + + /** + * Create a new empty instance of {@link HttpResponse}.
+ * + * @return A new {@link HttpResponse} instance. + */ + static HttpResponse httpResponse() + { + return FACTORY.httpResponse(); + } + + /** + * Create a new instance of {@link HttpResponse}.
+ * + * @param response The HTTP response. + * + * @return A new {@link HttpResponse} instance. + */ + static HttpResponse httpResponse(ByteArray response) + { + return FACTORY.httpResponse(response); + } + + /** + * Create a new instance of {@link HttpResponse}.
+ * + * @param response The HTTP response. + * + * @return A new {@link HttpResponse} instance. + */ + static HttpResponse httpResponse(String response) + { + return FACTORY.httpResponse(response); + } +} diff --git a/src/burp/api/montoya/http/message/responses/analysis/Attribute.java b/src/burp/api/montoya/http/message/responses/analysis/Attribute.java new file mode 100644 index 0000000..82c5693 --- /dev/null +++ b/src/burp/api/montoya/http/message/responses/analysis/Attribute.java @@ -0,0 +1,25 @@ +/* + * Copyright (c) 2022-2023. PortSwigger Ltd. All rights reserved. + * + * This code may be used to extend the functionality of Burp Suite Community Edition + * and Burp Suite Professional, provided that this usage does not violate the + * license terms for those products. + */ + +package burp.api.montoya.http.message.responses.analysis; + +/** + * Burp attribute able to retrieve to hold details about HTTP response attributes. + */ +public interface Attribute +{ + /** + * @return The attribute type. + */ + AttributeType type(); + + /** + * @return The attribute value. + */ + int value(); +} diff --git a/src/burp/api/montoya/http/message/responses/analysis/AttributeType.java b/src/burp/api/montoya/http/message/responses/analysis/AttributeType.java new file mode 100644 index 0000000..0c3614b --- /dev/null +++ b/src/burp/api/montoya/http/message/responses/analysis/AttributeType.java @@ -0,0 +1,47 @@ +/* + * Copyright (c) 2022-2023. PortSwigger Ltd. All rights reserved. + * + * This code may be used to extend the functionality of Burp Suite Community Edition + * and Burp Suite Professional, provided that this usage does not violate the + * license terms for those products. + */ + +package burp.api.montoya.http.message.responses.analysis; + +/** + * Otions that Burp can use to query attributes of HTTP responses. + */ +public enum AttributeType +{ + STATUS_CODE, + ETAG_HEADER, + LAST_MODIFIED_HEADER, + CONTENT_TYPE, + CONTENT_LENGTH, + COOKIE_NAMES, + TAG_NAMES, + TAG_IDS, + DIV_IDS, + BODY_CONTENT, + VISIBLE_TEXT, + WORD_COUNT, + VISIBLE_WORD_COUNT, + COMMENTS, + INITIAL_CONTENT, + CANONICAL_LINK, + PAGE_TITLE, + FIRST_HEADER_TAG, + HEADER_TAGS, + ANCHOR_LABELS, + INPUT_SUBMIT_LABELS, + BUTTON_SUBMIT_LABELS, + CSS_CLASSES, + LINE_COUNT, + LIMITED_BODY_CONTENT, + OUTBOUND_EDGE_COUNT, + OUTBOUND_EDGE_TAG_NAMES, + INPUT_IMAGE_LABELS, + CONTENT_LOCATION, + LOCATION, + NON_HIDDEN_FORM_INPUT_TYPES +} diff --git a/src/burp/api/montoya/http/message/responses/analysis/KeywordCount.java b/src/burp/api/montoya/http/message/responses/analysis/KeywordCount.java new file mode 100644 index 0000000..324353c --- /dev/null +++ b/src/burp/api/montoya/http/message/responses/analysis/KeywordCount.java @@ -0,0 +1,25 @@ +/* + * Copyright (c) 2022-2023. PortSwigger Ltd. All rights reserved. + * + * This code may be used to extend the functionality of Burp Suite Community Edition + * and Burp Suite Professional, provided that this usage does not violate the + * license terms for those products. + */ + +package burp.api.montoya.http.message.responses.analysis; + +/** + * Stores the number of types a given keyword appeared in a response. + */ +public interface KeywordCount +{ + /** + * @return The keyword. + */ + String keyword(); + + /** + * @return The number of times the keyword appeared in a response. + */ + int count(); +} diff --git a/src/burp/api/montoya/http/message/responses/analysis/ResponseKeywordsAnalyzer.java b/src/burp/api/montoya/http/message/responses/analysis/ResponseKeywordsAnalyzer.java new file mode 100644 index 0000000..b33bc6d --- /dev/null +++ b/src/burp/api/montoya/http/message/responses/analysis/ResponseKeywordsAnalyzer.java @@ -0,0 +1,36 @@ +/* + * Copyright (c) 2022-2023. PortSwigger Ltd. All rights reserved. + * + * This code may be used to extend the functionality of Burp Suite Community Edition + * and Burp Suite Professional, provided that this usage does not violate the + * license terms for those products. + */ + +package burp.api.montoya.http.message.responses.analysis; + +import burp.api.montoya.http.message.responses.HttpResponse; + +import java.util.Set; + +/** + * Analyze HTTP responses and retrieve keywords. + */ +public interface ResponseKeywordsAnalyzer +{ + /** + * @return A set of keywords whose counts vary between the analyzed responses. + */ + Set variantKeywords(); + + /** + * @return A set of keywords whose counts do not vary between the analyzed responses. + */ + Set invariantKeywords(); + + /** + * Update the analysis based on an additional response. + * + * @param response The new response to include in the analysis. + */ + void updateWith(HttpResponse response); +} diff --git a/src/burp/api/montoya/http/message/responses/analysis/ResponseVariationsAnalyzer.java b/src/burp/api/montoya/http/message/responses/analysis/ResponseVariationsAnalyzer.java new file mode 100644 index 0000000..039f5fa --- /dev/null +++ b/src/burp/api/montoya/http/message/responses/analysis/ResponseVariationsAnalyzer.java @@ -0,0 +1,36 @@ +/* + * Copyright (c) 2022-2023. PortSwigger Ltd. All rights reserved. + * + * This code may be used to extend the functionality of Burp Suite Community Edition + * and Burp Suite Professional, provided that this usage does not violate the + * license terms for those products. + */ + +package burp.api.montoya.http.message.responses.analysis; + +import burp.api.montoya.http.message.responses.HttpResponse; + +import java.util.Set; + +/** + * Analyze HTTP responses and find variations between them, according to various attributes. + */ +public interface ResponseVariationsAnalyzer +{ + /** + * @return The attributes that vary between the analyzed responses. + */ + Set variantAttributes(); + + /** + * @return The attributes that do not vary between the analyzed responses. + */ + Set invariantAttributes(); + + /** + * Update the analysis based on an additional response. + * + * @param response The new response to include in the analysis. + */ + void updateWith(HttpResponse response); +} diff --git a/src/burp/api/montoya/http/sessions/ActionResult.java b/src/burp/api/montoya/http/sessions/ActionResult.java new file mode 100644 index 0000000..9451262 --- /dev/null +++ b/src/burp/api/montoya/http/sessions/ActionResult.java @@ -0,0 +1,56 @@ +/* + * Copyright (c) 2022-2023. PortSwigger Ltd. All rights reserved. + * + * This code may be used to extend the functionality of Burp Suite Community Edition + * and Burp Suite Professional, provided that this usage does not violate the + * license terms for those products. + */ + +package burp.api.montoya.http.sessions; + +import burp.api.montoya.core.Annotations; +import burp.api.montoya.http.message.requests.HttpRequest; + +import static burp.api.montoya.internal.ObjectFactoryLocator.FACTORY; + +/** + * An instance of this interface should be returned by {@link SessionHandlingAction#performAction(SessionHandlingActionData)}. + */ +public interface ActionResult +{ + /** + * @return The HTTP request. + */ + HttpRequest request(); + + /** + * @return The annotations. + */ + Annotations annotations(); + + /** + * Create a new instance of {@code ActionResult}.
+ * Annotations will not be modified. + * + * @param request An HTTP request. + * + * @return A new {@code ActionResult} instance. + */ + static ActionResult actionResult(HttpRequest request) + { + return FACTORY.actionResult(request); + } + + /** + * Create a new instance of {@code ActionResult}. + * + * @param request An HTTP request. + * @param annotations modified annotations. + * + * @return A new {@code ActionResult} instance. + */ + static ActionResult actionResult(HttpRequest request, Annotations annotations) + { + return FACTORY.actionResult(request, annotations); + } +} diff --git a/src/burp/api/montoya/http/sessions/CookieJar.java b/src/burp/api/montoya/http/sessions/CookieJar.java new file mode 100644 index 0000000..edccc1e --- /dev/null +++ b/src/burp/api/montoya/http/sessions/CookieJar.java @@ -0,0 +1,36 @@ +/* + * Copyright (c) 2022-2023. PortSwigger Ltd. All rights reserved. + * + * This code may be used to extend the functionality of Burp Suite Community Edition + * and Burp Suite Professional, provided that this usage does not violate the + * license terms for those products. + */ + +package burp.api.montoya.http.sessions; + +import burp.api.montoya.http.message.Cookie; + +import java.time.ZonedDateTime; +import java.util.List; + +/** + * Provides access to Burp's Cookie Jar functionality. + */ +public interface CookieJar +{ + /** + * Add a new HTTP cookie to the Cookie Jar. + * + * @param name The name of the cookie. + * @param value The value of the cookie. + * @param path The path for which the cookie is in scope or {@code null} if none is set. + * @param domain The domain for which the cookie is in scope. + * @param expiration The expiration time for the cookie, or {@code null} if none is set (i.e., for non-persistent session cookies). + */ + void setCookie(String name, String value, String path, String domain, ZonedDateTime expiration); + + /** + * @return A list of stored cookies. + */ + List cookies(); +} diff --git a/src/burp/api/montoya/http/sessions/SessionHandlingAction.java b/src/burp/api/montoya/http/sessions/SessionHandlingAction.java new file mode 100644 index 0000000..fa1767c --- /dev/null +++ b/src/burp/api/montoya/http/sessions/SessionHandlingAction.java @@ -0,0 +1,35 @@ +/* + * Copyright (c) 2022-2023. PortSwigger Ltd. All rights reserved. + * + * This code may be used to extend the functionality of Burp Suite Community Edition + * and Burp Suite Professional, provided that this usage does not violate the + * license terms for those products. + */ + +package burp.api.montoya.http.sessions; + +import burp.api.montoya.http.Http; + +/** + * Extensions can implement this interface and then call {@link Http#registerSessionHandlingAction} to register a custom session handling action. Each registered action will be + * available within the session handling rule UI for the user to select as a rule action. Users can choose to invoke an action directly in its own right, or following execution of + * a macro. + */ +public interface SessionHandlingAction +{ + /** + * @return Action name + */ + String name(); + + /** + * Invoked when the session handling action should be executed.
+ * This may happen as an action in its own right, or as a sub-action following execution of a macro.
+ * It can issue additional requests of its own if necessary, and can return a modified base request in the {@link ActionResult} + * + * @param actionData {@link SessionHandlingActionData} The action can query this object to obtain details about the base request. + * + * @return A new {@link ActionResult} instance. + */ + ActionResult performAction(SessionHandlingActionData actionData); +} diff --git a/src/burp/api/montoya/http/sessions/SessionHandlingActionData.java b/src/burp/api/montoya/http/sessions/SessionHandlingActionData.java new file mode 100644 index 0000000..d5c2c3f --- /dev/null +++ b/src/burp/api/montoya/http/sessions/SessionHandlingActionData.java @@ -0,0 +1,39 @@ +/* + * Copyright (c) 2022-2023. PortSwigger Ltd. All rights reserved. + * + * This code may be used to extend the functionality of Burp Suite Community Edition + * and Burp Suite Professional, provided that this usage does not violate the + * license terms for those products. + */ + +package burp.api.montoya.http.sessions; + +import burp.api.montoya.core.Annotations; +import burp.api.montoya.http.message.HttpRequestResponse; +import burp.api.montoya.http.message.requests.HttpRequest; + +import java.util.List; + +/** + * Information required for session handling. + */ +public interface SessionHandlingActionData +{ + /** + * @return The base request that is currently being processed. + */ + HttpRequest request(); + + /** + * If the action is invoked following execution of a macro, this method contains the result of executing the macro. Otherwise, it is an empty list. Actions can use the details + * of the macro items to perform custom analysis of the macro to derive values of non-standard session handling tokens, etc. + * + * @return List of {@link HttpRequestResponse} generated during the execution of the macro. + */ + List macroRequestResponses(); + + /** + * @return The message annotation on the request. + */ + Annotations annotations(); +} diff --git a/src/burp/api/montoya/internal/MontoyaObjectFactory.java b/src/burp/api/montoya/internal/MontoyaObjectFactory.java new file mode 100644 index 0000000..2729457 --- /dev/null +++ b/src/burp/api/montoya/internal/MontoyaObjectFactory.java @@ -0,0 +1,326 @@ +/* + * Copyright (c) 2022-2023. PortSwigger Ltd. All rights reserved. + * + * This code may be used to extend the functionality of Burp Suite Community Edition + * and Burp Suite Professional, provided that this usage does not violate the + * license terms for those products. + */ + +package burp.api.montoya.internal; + +import burp.api.montoya.collaborator.InteractionFilter; +import burp.api.montoya.collaborator.SecretKey; +import burp.api.montoya.core.Annotations; +import burp.api.montoya.core.ByteArray; +import burp.api.montoya.core.HighlightColor; +import burp.api.montoya.core.Marker; +import burp.api.montoya.core.Range; +import burp.api.montoya.http.HttpService; +import burp.api.montoya.http.RequestOptions; +import burp.api.montoya.http.handler.RequestToBeSentAction; +import burp.api.montoya.http.handler.ResponseReceivedAction; +import burp.api.montoya.http.message.HttpHeader; +import burp.api.montoya.http.message.HttpRequestResponse; +import burp.api.montoya.http.message.params.HttpParameter; +import burp.api.montoya.http.message.params.HttpParameterType; +import burp.api.montoya.http.message.requests.HttpRequest; +import burp.api.montoya.http.message.responses.HttpResponse; +import burp.api.montoya.http.sessions.ActionResult; +import burp.api.montoya.intruder.GeneratedPayload; +import burp.api.montoya.intruder.HttpRequestTemplate; +import burp.api.montoya.intruder.HttpRequestTemplateGenerationOptions; +import burp.api.montoya.intruder.PayloadProcessingAction; +import burp.api.montoya.intruder.PayloadProcessingResult; +import burp.api.montoya.persistence.PersistedList; +import burp.api.montoya.persistence.PersistedObject; +import burp.api.montoya.proxy.MessageReceivedAction; +import burp.api.montoya.proxy.MessageToBeSentAction; +import burp.api.montoya.proxy.http.ProxyRequestReceivedAction; +import burp.api.montoya.proxy.http.ProxyRequestToBeSentAction; +import burp.api.montoya.proxy.http.ProxyResponseReceivedAction; +import burp.api.montoya.proxy.http.ProxyResponseToBeSentAction; +import burp.api.montoya.proxy.websocket.BinaryMessageReceivedAction; +import burp.api.montoya.proxy.websocket.BinaryMessageToBeSentAction; +import burp.api.montoya.proxy.websocket.TextMessageReceivedAction; +import burp.api.montoya.proxy.websocket.TextMessageToBeSentAction; +import burp.api.montoya.scanner.AuditConfiguration; +import burp.api.montoya.scanner.AuditResult; +import burp.api.montoya.scanner.BuiltInAuditConfiguration; +import burp.api.montoya.scanner.CrawlConfiguration; +import burp.api.montoya.scanner.audit.insertionpoint.AuditInsertionPoint; +import burp.api.montoya.scanner.audit.issues.AuditIssue; +import burp.api.montoya.scanner.audit.issues.AuditIssueConfidence; +import burp.api.montoya.scanner.audit.issues.AuditIssueDefinition; +import burp.api.montoya.scanner.audit.issues.AuditIssueSeverity; +import burp.api.montoya.sitemap.SiteMapFilter; +import burp.api.montoya.ui.Selection; +import burp.api.montoya.ui.menu.BasicMenuItem; +import burp.api.montoya.ui.menu.Menu; +import burp.api.montoya.websocket.BinaryMessageAction; +import burp.api.montoya.websocket.MessageAction; +import burp.api.montoya.websocket.TextMessageAction; + +import java.util.List; + +public interface MontoyaObjectFactory +{ + HttpService httpService(String baseUrl); + + HttpService httpService(String host, boolean secure); + + HttpService httpService(String host, int port, boolean secure); + + HttpHeader httpHeader(String name, String value); + + HttpHeader httpHeader(String header); + + HttpParameter parameter(String name, String value, HttpParameterType type); + + HttpRequest httpRequest(); + + HttpRequest httpRequest(ByteArray request); + + HttpRequest httpRequest(String request); + + HttpRequest httpRequest(HttpService service, ByteArray request); + + HttpRequest httpRequest(HttpService service, String request); + + HttpRequest http2Request(HttpService service, List headers, String body); + + HttpRequest http2Request(HttpService service, List headers, ByteArray body); + + HttpRequest httpRequestFromUrl(String url); + + HttpResponse httpResponse(); + + HttpResponse httpResponse(String response); + + HttpResponse httpResponse(ByteArray response); + + HttpRequestResponse httpRequestResponse(HttpRequest request, HttpResponse response, Annotations annotations); + + HttpRequestResponse httpRequestResponse(HttpRequest request, HttpResponse response); + + Range range(int startIndexInclusive, int endIndexExclusive); + + Annotations annotations(); + + Annotations annotations(String notes); + + Annotations annotations(HighlightColor highlightColor); + + Annotations annotations(String notes, HighlightColor highlightColor); + + AuditInsertionPoint auditInsertionPoint(String name, HttpRequest baseRequest, int startIndexInclusive, int endIndexExclusive); + + AuditIssueDefinition auditIssueDefinition(String name, String background, String remediation, AuditIssueSeverity typicalSeverity); + + AuditIssue auditIssue( + String name, + String detail, + String remediation, + String baseUrl, + AuditIssueSeverity severity, + AuditIssueConfidence confidence, + String background, + String remediationBackground, + AuditIssueSeverity typicalSeverity, + List requestResponses); + + AuditIssue auditIssue( + String name, + String detail, + String remediation, + String baseUrl, + AuditIssueSeverity severity, + AuditIssueConfidence confidence, + String background, + String remediationBackground, + AuditIssueSeverity typicalSeverity, + HttpRequestResponse... requestResponses); + + Selection selection(ByteArray selectionContents); + + Selection selection(int startIndexInclusive, int endIndexExclusive); + + Selection selection(ByteArray selectionContents, int startIndexInclusive, int endIndexExclusive); + + SecretKey secretKey(String encodedKey); + + ProxyRequestReceivedAction proxyRequestReceivedAction(HttpRequest request, Annotations annotations, MessageReceivedAction action); + + ProxyRequestToBeSentAction proxyRequestToBeSentAction(HttpRequest request, Annotations annotations, MessageToBeSentAction action); + + ProxyResponseToBeSentAction proxyResponseToReturnAction(HttpResponse response, Annotations annotations, MessageToBeSentAction action); + + ProxyResponseReceivedAction proxyResponseReceivedAction(HttpResponse response, Annotations annotations, MessageReceivedAction action); + + RequestToBeSentAction requestResult(HttpRequest request, Annotations annotations); + + ResponseReceivedAction responseResult(HttpResponse response, Annotations annotations); + + HttpRequestTemplate httpRequestTemplate(ByteArray content, List insertionPointOffsets); + + HttpRequestTemplate httpRequestTemplate(HttpRequest request, List insertionPointOffsets); + + HttpRequestTemplate httpRequestTemplate(ByteArray content, HttpRequestTemplateGenerationOptions options); + + HttpRequestTemplate httpRequestTemplate(HttpRequest request, HttpRequestTemplateGenerationOptions options); + + PayloadProcessingResult payloadProcessingResult(ByteArray processedPayload, PayloadProcessingAction action); + + InteractionFilter interactionIdFilter(String id); + + InteractionFilter interactionPayloadFilter(String payload); + + SiteMapFilter prefixFilter(String prefix); + + Marker marker(Range range); + + Marker marker(int startIndexInclusive, int endIndexExclusive); + + ByteArray byteArrayOfLength(int length); + + ByteArray byteArray(byte[] bytes); + + ByteArray byteArray(int[] ints); + + ByteArray byteArray(String text); + + TextMessageAction continueWithTextMessage(String payload); + + TextMessageAction dropTextMessage(); + + TextMessageAction textMessageAction(String payload, MessageAction action); + + BinaryMessageAction continueWithBinaryMessage(ByteArray payload); + + BinaryMessageAction dropBinaryMessage(); + + BinaryMessageAction binaryMessageAction(ByteArray payload, MessageAction action); + + BinaryMessageReceivedAction followUserRulesInitialProxyBinaryMessage(ByteArray payload); + + TextMessageReceivedAction followUserRulesInitialProxyTextMessage(String payload); + + BinaryMessageReceivedAction interceptInitialProxyBinaryMessage(ByteArray payload); + + TextMessageReceivedAction interceptInitialProxyTextMessage(String payload); + + BinaryMessageReceivedAction dropInitialProxyBinaryMessage(); + + TextMessageReceivedAction dropInitialProxyTextMessage(); + + BinaryMessageReceivedAction doNotInterceptInitialProxyBinaryMessage(ByteArray payload); + + TextMessageReceivedAction doNotInterceptInitialProxyTextMessage(String payload); + + BinaryMessageToBeSentAction continueWithFinalProxyBinaryMessage(ByteArray payload); + + TextMessageToBeSentAction continueWithFinalProxyTextMessage(String payload); + + BinaryMessageToBeSentAction dropFinalProxyBinaryMessage(); + + TextMessageToBeSentAction dropFinalProxyTextMessage(); + + PersistedObject persistedObject(); + + PersistedList persistedBooleanList(); + + PersistedList persistedShortList(); + + PersistedList persistedIntegerList(); + + PersistedList persistedLongList(); + + PersistedList persistedStringList(); + + PersistedList persistedByteArrayList(); + + PersistedList persistedHttpRequestList(); + + PersistedList persistedHttpResponseList(); + + PersistedList persistedHttpRequestResponseList(); + + AuditResult auditResult(List auditIssues); + + AuditResult auditResult(AuditIssue... auditIssues); + + AuditConfiguration auditConfiguration(BuiltInAuditConfiguration builtInAuditConfiguration); + + CrawlConfiguration crawlConfiguration(String... seedUrls); + + HttpParameter urlParameter(String name, String value); + + HttpParameter bodyParameter(String name, String value); + + HttpParameter cookieParameter(String name, String value); + + GeneratedPayload payload(String payload); + + GeneratedPayload payload(ByteArray payload); + + GeneratedPayload payloadEnd(); + + PayloadProcessingResult usePayload(ByteArray processedPayload); + + PayloadProcessingResult skipPayload(); + + ProxyRequestToBeSentAction requestFinalInterceptResultContinueWith(HttpRequest request); + + ProxyRequestToBeSentAction requestFinalInterceptResultContinueWith(HttpRequest request, Annotations annotations); + + ProxyRequestToBeSentAction requestFinalInterceptResultDrop(); + + ProxyResponseToBeSentAction responseFinalInterceptResultDrop(); + + ProxyResponseToBeSentAction responseFinalInterceptResultContinueWith(HttpResponse response, Annotations annotations); + + ProxyResponseToBeSentAction responseFinalInterceptResultContinueWith(HttpResponse response); + + ProxyResponseReceivedAction responseInitialInterceptResultIntercept(HttpResponse response); + + ProxyResponseReceivedAction responseInitialInterceptResultIntercept(HttpResponse response, Annotations annotations); + + ProxyResponseReceivedAction responseInitialInterceptResultDoNotIntercept(HttpResponse response); + + ProxyResponseReceivedAction responseInitialInterceptResultDoNotIntercept(HttpResponse response, Annotations annotations); + + ProxyResponseReceivedAction responseInitialInterceptResultFollowUserRules(HttpResponse response); + + ProxyResponseReceivedAction responseInitialInterceptResultFollowUserRules(HttpResponse response, Annotations annotations); + + ProxyResponseReceivedAction responseInitialInterceptResultDrop(); + + ProxyRequestReceivedAction requestInitialInterceptResultIntercept(HttpRequest request); + + ProxyRequestReceivedAction requestInitialInterceptResultIntercept(HttpRequest request, Annotations annotations); + + ProxyRequestReceivedAction requestInitialInterceptResultDoNotIntercept(HttpRequest request); + + ProxyRequestReceivedAction requestInitialInterceptResultDoNotIntercept(HttpRequest request, Annotations annotations); + + ProxyRequestReceivedAction requestInitialInterceptResultFollowUserRules(HttpRequest request); + + ProxyRequestReceivedAction requestInitialInterceptResultFollowUserRules(HttpRequest request, Annotations annotations); + + ProxyRequestReceivedAction requestInitialInterceptResultDrop(); + + ResponseReceivedAction responseResult(HttpResponse response); + + RequestToBeSentAction requestResult(HttpRequest request); + + HighlightColor highlightColor(String color); + + ActionResult actionResult(HttpRequest request); + + ActionResult actionResult(HttpRequest request, Annotations annotations); + + Menu menu(String caption); + + BasicMenuItem basicMenuItem(String caption); + + RequestOptions requestOptions(); +} diff --git a/src/burp/api/montoya/internal/ObjectFactoryLocator.java b/src/burp/api/montoya/internal/ObjectFactoryLocator.java new file mode 100644 index 0000000..972175e --- /dev/null +++ b/src/burp/api/montoya/internal/ObjectFactoryLocator.java @@ -0,0 +1,17 @@ +/* + * Copyright (c) 2022-2023. PortSwigger Ltd. All rights reserved. + * + * This code may be used to extend the functionality of Burp Suite Community Edition + * and Burp Suite Professional, provided that this usage does not violate the + * license terms for those products. + */ + +package burp.api.montoya.internal; + +public class ObjectFactoryLocator +{ + /** + * This is initialized when your extension is loaded. + */ + public static MontoyaObjectFactory FACTORY = null; +} diff --git a/src/burp/api/montoya/intruder/AttackConfiguration.java b/src/burp/api/montoya/intruder/AttackConfiguration.java new file mode 100644 index 0000000..42c257d --- /dev/null +++ b/src/burp/api/montoya/intruder/AttackConfiguration.java @@ -0,0 +1,34 @@ +/* + * Copyright (c) 2022-2023. PortSwigger Ltd. All rights reserved. + * + * This code may be used to extend the functionality of Burp Suite Community Edition + * and Burp Suite Professional, provided that this usage does not violate the + * license terms for those products. + */ + +package burp.api.montoya.intruder; + +import burp.api.montoya.http.HttpService; + +import java.util.Optional; + +/** + * Intruder attack configuration. + */ +public interface AttackConfiguration +{ + /** + * {@link HttpService} for the attack. + * + * @return An {@link Optional} of {@link HttpService} instance derived from this attack configuration or {@link Optional#empty} if the target template contains payload markers. + */ + Optional httpService(); + + /** + * HTTP request template and insertion point offsets in a + * form of an {@link HttpRequestTemplate} instance. + * + * @return An instance of {@link HttpRequestTemplate}. + */ + HttpRequestTemplate requestTemplate(); +} diff --git a/src/burp/api/montoya/intruder/GeneratedPayload.java b/src/burp/api/montoya/intruder/GeneratedPayload.java new file mode 100644 index 0000000..3b4c52b --- /dev/null +++ b/src/burp/api/montoya/intruder/GeneratedPayload.java @@ -0,0 +1,58 @@ +/* + * Copyright (c) 2022-2023. PortSwigger Ltd. All rights reserved. + * + * This code may be used to extend the functionality of Burp Suite Community Edition + * and Burp Suite Professional, provided that this usage does not violate the + * license terms for those products. + */ + +package burp.api.montoya.intruder; + +import burp.api.montoya.core.ByteArray; + +import static burp.api.montoya.internal.ObjectFactoryLocator.FACTORY; + +/** + * Intruder payload. + */ +public interface GeneratedPayload +{ + /** + * @return Payload value. + */ + ByteArray value(); + + /** + * Create a new {@link GeneratedPayload} instance from a String payload value. + * + * @param payload String payload value. + * + * @return A new {@link GeneratedPayload} instance. + */ + static GeneratedPayload payload(String payload) + { + return FACTORY.payload(payload); + } + + /** + * Create a new {@link GeneratedPayload} instance from a byte array payload value. + * + * @param payload Byte array payload value. + * + * @return A new {@link GeneratedPayload} instance. + */ + static GeneratedPayload payload(ByteArray payload) + { + return FACTORY.payload(payload); + } + + /** + * Create a new {@link GeneratedPayload} instance to signify there are no more payloads. + * + * @return A new {@link GeneratedPayload} instance. + */ + static GeneratedPayload end() + { + return FACTORY.payloadEnd(); + } +} diff --git a/src/burp/api/montoya/intruder/HttpRequestTemplate.java b/src/burp/api/montoya/intruder/HttpRequestTemplate.java new file mode 100644 index 0000000..70cb07a --- /dev/null +++ b/src/burp/api/montoya/intruder/HttpRequestTemplate.java @@ -0,0 +1,91 @@ +/* + * Copyright (c) 2022-2023. PortSwigger Ltd. All rights reserved. + * + * This code may be used to extend the functionality of Burp Suite Community Edition + * and Burp Suite Professional, provided that this usage does not violate the + * license terms for those products. + */ + +package burp.api.montoya.intruder; + +import burp.api.montoya.core.ByteArray; +import burp.api.montoya.core.Range; +import burp.api.montoya.http.message.requests.HttpRequest; + +import java.util.List; + +import static burp.api.montoya.internal.ObjectFactoryLocator.FACTORY; + +/** + * Intruder request template, which contains the HTTP request and insertion point offsets. + */ +public interface HttpRequestTemplate +{ + /** + * @return Content of the request template. + */ + ByteArray content(); + + /** + * Insertion point offsets for an Intruder attack. + * + * @return A list of {@link Range} objects representing insertion point offsets. + */ + List insertionPointOffsets(); + + /** + * Create a new {@link HttpRequestTemplate} instance + * from an {@link HttpRequest} object and a list of insertion point offsets. + * + * @param request An instance of {@link HttpRequest}. + * @param insertionPointOffsets List of insertion point offsets. + * + * @return A new instance of {@link HttpRequestTemplate}. + */ + static HttpRequestTemplate httpRequestTemplate(HttpRequest request, List insertionPointOffsets) + { + return FACTORY.httpRequestTemplate(request, insertionPointOffsets); + } + + /** + * Create a new {@link HttpRequestTemplate} instance + * from an HTTP request in a byte array form and a list of insertion point offsets. + * + * @param content An HTTP request in a byte array form. + * @param insertionPointOffsets List of insertion point offsets. + * + * @return A new instance of {@link HttpRequestTemplate}. + */ + static HttpRequestTemplate httpRequestTemplate(ByteArray content, List insertionPointOffsets) + { + return FACTORY.httpRequestTemplate(content, insertionPointOffsets); + } + + /** + * Create a new {@link HttpRequestTemplate} instance + * from an {@link HttpRequest} object with insertion point offsets at each URL, cookie, and body parameter position. + * + * @param request An instance of {@link HttpRequest}. + * @param options Options to use when generating the template. + * + * @return A new instance of {@link HttpRequestTemplate}. + */ + static HttpRequestTemplate httpRequestTemplate(HttpRequest request, HttpRequestTemplateGenerationOptions options) + { + return FACTORY.httpRequestTemplate(request, options); + } + + /** + * Create a new {@link HttpRequestTemplate} instance + * from an HTTP request in a byte array form with insertion point offsets at each URL, cookie, and body parameter position. + * + * @param content An HTTP request in a byte array form. + * @param options Options to use when generating the template. + * + * @return A new instance of {@link HttpRequestTemplate}. + */ + static HttpRequestTemplate httpRequestTemplate(ByteArray content, HttpRequestTemplateGenerationOptions options) + { + return FACTORY.httpRequestTemplate(content, options); + } +} diff --git a/src/burp/api/montoya/intruder/HttpRequestTemplateGenerationOptions.java b/src/burp/api/montoya/intruder/HttpRequestTemplateGenerationOptions.java new file mode 100644 index 0000000..ca842b0 --- /dev/null +++ b/src/burp/api/montoya/intruder/HttpRequestTemplateGenerationOptions.java @@ -0,0 +1,25 @@ +/* + * Copyright (c) 2023. PortSwigger Ltd. All rights reserved. + * + * This code may be used to extend the functionality of Burp Suite Community Edition + * and Burp Suite Professional, provided that this usage does not violate the + * license terms for those products. + */ + +package burp.api.montoya.intruder; + +/** + * Options that can be used to generate a new HttpRequestTemplate. + */ +public enum HttpRequestTemplateGenerationOptions +{ + /** + * Replace base parameter value with offsets. + */ + REPLACE_BASE_PARAMETER_VALUE_WITH_OFFSETS, + + /** + * Append offsets to base parameter value. + */ + APPEND_OFFSETS_TO_BASE_PARAMETER_VALUE +} diff --git a/src/burp/api/montoya/intruder/Intruder.java b/src/burp/api/montoya/intruder/Intruder.java new file mode 100644 index 0000000..c3426aa --- /dev/null +++ b/src/burp/api/montoya/intruder/Intruder.java @@ -0,0 +1,64 @@ +/* + * Copyright (c) 2022-2023. PortSwigger Ltd. All rights reserved. + * + * This code may be used to extend the functionality of Burp Suite Community Edition + * and Burp Suite Professional, provided that this usage does not violate the + * license terms for those products. + */ + +package burp.api.montoya.intruder; + +import burp.api.montoya.core.Registration; +import burp.api.montoya.http.HttpService; +import burp.api.montoya.http.message.requests.HttpRequest; + +/** + * Provides access to the functionality of the Burp Intruder tool. + */ +public interface Intruder +{ + /** + * Register a custom Intruder payload processor. Each registered + * processor will be available within the Intruder UI for the user to select as the + * action for a payload processing rule. + * + * @param payloadProcessor An object created by the extension that implements the + * {@link PayloadProcessor} interface. + * + * @return The {@link Registration} for the payload processor. + */ + Registration registerPayloadProcessor(PayloadProcessor payloadProcessor); + + /** + * Register a provider for Intruder payloads. Each registered + * provider will be available within the Intruder UI for the user to select as the payload + * source for an attack. When this is selected, the provider will be asked to provide a + * new instance of an {@link PayloadGenerator} object, which will be used to generate + * payloads for the attack. + * + * @param payloadGeneratorProvider An object created by the extension that implements the + * PayloadGeneratorProvider interface. + * + * @return The {@link Registration} for the payload generator provider. + */ + Registration registerPayloadGeneratorProvider(PayloadGeneratorProvider payloadGeneratorProvider); + + /** + * Send an HTTP request to the Burp Intruder tool. The request + * will be displayed in the user interface, and markers for attack payloads will be placed + * into the locations specified in the provided {@link HttpRequestTemplate} object. + * + * @param service An {@link HttpService} object that specifies the hostname, port and protocol + * of a remote server. + * @param requestTemplate An HTTP request template containing insertion point offsets. + */ + void sendToIntruder(HttpService service, HttpRequestTemplate requestTemplate); + + /** + * Send an HTTP request to the Burp Intruder tool. The request + * will be displayed in the user interface. + * + * @param request The full HTTP request. + */ + void sendToIntruder(HttpRequest request); +} diff --git a/src/burp/api/montoya/intruder/IntruderInsertionPoint.java b/src/burp/api/montoya/intruder/IntruderInsertionPoint.java new file mode 100644 index 0000000..19cef6f --- /dev/null +++ b/src/burp/api/montoya/intruder/IntruderInsertionPoint.java @@ -0,0 +1,22 @@ +/* + * Copyright (c) 2022-2023. PortSwigger Ltd. All rights reserved. + * + * This code may be used to extend the functionality of Burp Suite Community Edition + * and Burp Suite Professional, provided that this usage does not violate the + * license terms for those products. + */ + +package burp.api.montoya.intruder; + +import burp.api.montoya.core.ByteArray; + +/** + * Intruder insertion point for attack payloads. + */ +public interface IntruderInsertionPoint +{ + /** + * @return The base value of the insertion point. + */ + ByteArray baseValue(); +} diff --git a/src/burp/api/montoya/intruder/PayloadData.java b/src/burp/api/montoya/intruder/PayloadData.java new file mode 100644 index 0000000..6babe17 --- /dev/null +++ b/src/burp/api/montoya/intruder/PayloadData.java @@ -0,0 +1,32 @@ +/* + * Copyright (c) 2022-2023. PortSwigger Ltd. All rights reserved. + * + * This code may be used to extend the functionality of Burp Suite Community Edition + * and Burp Suite Professional, provided that this usage does not violate the + * license terms for those products. + */ + +package burp.api.montoya.intruder; + +import burp.api.montoya.core.ByteArray; + +/** + * Contains information about the payload + */ +public interface PayloadData +{ + /** + * @return The value of the payload to be processed. + */ + ByteArray currentPayload(); + + /** + * @return The value of the original payload prior to processing by any already-applied processing rules + */ + ByteArray originalPayload(); + + /** + * @return The insertion point data. + */ + IntruderInsertionPoint insertionPoint(); +} diff --git a/src/burp/api/montoya/intruder/PayloadGenerator.java b/src/burp/api/montoya/intruder/PayloadGenerator.java new file mode 100644 index 0000000..26783da --- /dev/null +++ b/src/burp/api/montoya/intruder/PayloadGenerator.java @@ -0,0 +1,27 @@ +/* + * Copyright (c) 2022-2023. PortSwigger Ltd. All rights reserved. + * + * This code may be used to extend the functionality of Burp Suite Community Edition + * and Burp Suite Professional, provided that this usage does not violate the + * license terms for those products. + */ + +package burp.api.montoya.intruder; + +/** + * Intruder payload generator. Extensions that have registered + * a {@link PayloadGeneratorProvider} must return a new instance of this interface when required as part + * of a new Intruder attack. + */ +public interface PayloadGenerator +{ + /** + * Invoked by Burp to obtain the value of the next payload. + * Should return {@link GeneratedPayload#end()} instance to signal to Burp that the generator has finished. + * + * @param insertionPoint Insertion point for the payload. + * + * @return A generated Intruder payload. + */ + GeneratedPayload generatePayloadFor(IntruderInsertionPoint insertionPoint); +} \ No newline at end of file diff --git a/src/burp/api/montoya/intruder/PayloadGeneratorProvider.java b/src/burp/api/montoya/intruder/PayloadGeneratorProvider.java new file mode 100644 index 0000000..ed6976d --- /dev/null +++ b/src/burp/api/montoya/intruder/PayloadGeneratorProvider.java @@ -0,0 +1,35 @@ +/* + * Copyright (c) 2022-2023. PortSwigger Ltd. All rights reserved. + * + * This code may be used to extend the functionality of Burp Suite Community Edition + * and Burp Suite Professional, provided that this usage does not violate the + * license terms for those products. + */ + +package burp.api.montoya.intruder; + +/** + * Extensions can implement this interface and then call {@link Intruder#registerPayloadGeneratorProvider} + * to register a provider for custom Intruder payload generators. + */ +public interface PayloadGeneratorProvider +{ + /** + * Name Burp will use when displaying the payload generator + * in a dropdown list in the UI. + * + * @return Name of the payload generator. + */ + String displayName(); + + /** + * Invoked by Burp to obtain an instance of {@link PayloadGenerator} + * to add to Intruder. + * + * @param attackConfiguration An object containing information about the currently + * selected attack configuration tab. + * + * @return An instance of an object that implements the {@link PayloadGenerator} interface. + */ + PayloadGenerator providePayloadGenerator(AttackConfiguration attackConfiguration); +} diff --git a/src/burp/api/montoya/intruder/PayloadProcessingAction.java b/src/burp/api/montoya/intruder/PayloadProcessingAction.java new file mode 100644 index 0000000..ba9255e --- /dev/null +++ b/src/burp/api/montoya/intruder/PayloadProcessingAction.java @@ -0,0 +1,24 @@ +/* + * Copyright (c) 2022-2023. PortSwigger Ltd. All rights reserved. + * + * This code may be used to extend the functionality of Burp Suite Community Edition + * and Burp Suite Professional, provided that this usage does not violate the + * license terms for those products. + */ + +package burp.api.montoya.intruder; + +/** + * Instructions that the payload processor can give Intruder for the current payload. + */ +public enum PayloadProcessingAction +{ + /** + * Skip the current payload + */ + SKIP_PAYLOAD, + /** + * Use the current payload + */ + USE_PAYLOAD +} diff --git a/src/burp/api/montoya/intruder/PayloadProcessingResult.java b/src/burp/api/montoya/intruder/PayloadProcessingResult.java new file mode 100644 index 0000000..228ec41 --- /dev/null +++ b/src/burp/api/montoya/intruder/PayloadProcessingResult.java @@ -0,0 +1,63 @@ +/* + * Copyright (c) 2022-2023. PortSwigger Ltd. All rights reserved. + * + * This code may be used to extend the functionality of Burp Suite Community Edition + * and Burp Suite Professional, provided that this usage does not violate the + * license terms for those products. + */ + +package burp.api.montoya.intruder; + +import burp.api.montoya.core.ByteArray; + +import static burp.api.montoya.internal.ObjectFactoryLocator.FACTORY; + +/** + * An instance of this interface should be returned by {@link PayloadProcessor#processPayload} if a custom + * {@link PayloadProcessor} was registered with Intruder. + */ +public interface PayloadProcessingResult +{ + /** + * @return The current value of the processed payload. + */ + ByteArray processedPayload(); + + /** + * Invoked by Burp to see what action it should perform with the payload. If the value + * is {@link PayloadProcessingAction#USE_PAYLOAD}, Burp will use the payload in the attack or skip it + * if the value is {@link PayloadProcessingAction#SKIP_PAYLOAD}. + * + * @return Action to perform with the payload. + */ + PayloadProcessingAction action(); + + /** + * Create a new instance of {@link PayloadProcessingResult} with a + * {@link PayloadProcessingAction#USE_PAYLOAD} action. + * + * @param processedPayload Processed payload value + * + * @return A new {@link PayloadProcessingResult} instance. + */ + static PayloadProcessingResult usePayload(ByteArray processedPayload) + { + return FACTORY.usePayload(processedPayload); + } + + /** + * Create a new instance of {@link PayloadProcessingResult} with a + * {@link PayloadProcessingAction#SKIP_PAYLOAD} action. + * + * @return A new {@link PayloadProcessingResult} instance. + */ + static PayloadProcessingResult skipPayload() + { + return FACTORY.skipPayload(); + } + + static PayloadProcessingResult payloadProcessingResult(ByteArray processedPayload, PayloadProcessingAction action) + { + return FACTORY.payloadProcessingResult(processedPayload, action); + } +} diff --git a/src/burp/api/montoya/intruder/PayloadProcessor.java b/src/burp/api/montoya/intruder/PayloadProcessor.java new file mode 100644 index 0000000..512f6a2 --- /dev/null +++ b/src/burp/api/montoya/intruder/PayloadProcessor.java @@ -0,0 +1,33 @@ +/* + * Copyright (c) 2022-2023. PortSwigger Ltd. All rights reserved. + * + * This code may be used to extend the functionality of Burp Suite Community Edition + * and Burp Suite Professional, provided that this usage does not violate the + * license terms for those products. + */ + +package burp.api.montoya.intruder; + +/** + * Extensions can implement this interface and then call {@link Intruder#registerPayloadProcessor} to register a + * custom Intruder payload processor. + */ +public interface PayloadProcessor +{ + /** + * Name Burp will use when displaying the payload processor + * in a dropdown list in the UI. + * + * @return Name of the payload processor + */ + String displayName(); + + /** + * Invoked by Burp each time the processor should be applied to an Intruder payload. + * + * @param payloadData Information about the current payload to be processed + * + * @return The value of the processed payload. + */ + PayloadProcessingResult processPayload(PayloadData payloadData); +} diff --git a/src/burp/api/montoya/logger/LoggerHttpRequestResponse.java b/src/burp/api/montoya/logger/LoggerHttpRequestResponse.java new file mode 100644 index 0000000..00ed114 --- /dev/null +++ b/src/burp/api/montoya/logger/LoggerHttpRequestResponse.java @@ -0,0 +1,108 @@ +/* + * Copyright (c) 2022-2023. PortSwigger Ltd. All rights reserved. + * + * This code may be used to extend the functionality of Burp Suite Community Edition + * and Burp Suite Professional, provided that this usage does not violate the + * license terms for those products. + */ + +package burp.api.montoya.logger; + +import burp.api.montoya.core.Annotations; +import burp.api.montoya.core.ToolSource; +import burp.api.montoya.http.HttpService; +import burp.api.montoya.http.handler.TimingData; +import burp.api.montoya.http.message.MimeType; +import burp.api.montoya.http.message.requests.HttpRequest; +import burp.api.montoya.http.message.responses.HttpResponse; + +import java.time.ZonedDateTime; +import java.util.regex.Pattern; + +/** + * This interface is used to define a coupling between {@link HttpRequest} and {@link HttpResponse} for the Logger. + */ +public interface LoggerHttpRequestResponse +{ + /** + * @return The HTTP request message. + */ + HttpRequest request(); + + /** + * @return The HTTP response message. + */ + HttpResponse response(); + + /** + * HTTP service for the request. + * + * @return An {@link HttpService} object containing details of the HTTP service. + */ + HttpService httpService(); + + /** + * @return The annotations. + */ + Annotations annotations(); + + /** + * Returns the date and time at which Burp Logger received the request. + * + * @return The time at which Burp Logger received the request. + */ + ZonedDateTime time(); + + /** + * Obtain the MIME type of the response or request, as determined by Burp Suite. + * If there is no response the mime type will be determined from the request url. + * + * @return The MIME type. + */ + MimeType mimeType(); + + /** + * @return True if there is a response. + */ + boolean hasResponse(); + + /** + * Retrieve the timing data associated with this request. + * + * @return The timing data. + */ + TimingData timingData(); + + /** + * The page title for the response. + * + * @return The page title, or an empty string if none exists. + */ + String pageTitle(); + + /** + * The tool that issued the request. + * + * @return ToolSource which indicates which Burp tool sent the request. + */ + ToolSource toolSource(); + + /** + * Searches the data in the HTTP request, response and notes for the specified search term. + * + * @param searchTerm The value to be searched for. + * @param caseSensitive Flags whether the search is case-sensitive. + * + * @return True if the search term is found. + */ + boolean contains(String searchTerm, boolean caseSensitive); + + /** + * Searches the data in the HTTP request, response and notes for the specified regular expression. + * + * @param pattern The regular expression to be searched for. + * + * @return True if the pattern is matched. + */ + boolean contains(Pattern pattern); +} diff --git a/src/burp/api/montoya/logging/Logging.java b/src/burp/api/montoya/logging/Logging.java new file mode 100644 index 0000000..a372545 --- /dev/null +++ b/src/burp/api/montoya/logging/Logging.java @@ -0,0 +1,107 @@ +/* + * Copyright (c) 2022-2023. PortSwigger Ltd. All rights reserved. + * + * This code may be used to extend the functionality of Burp Suite Community Edition + * and Burp Suite Professional, provided that this usage does not violate the + * license terms for those products. + */ + +package burp.api.montoya.logging; + +import java.io.PrintStream; + +/** + * Provides access to the functionality related to logging and events. + */ +public interface Logging +{ + /** + * Obtain the current extension's standard output + * stream. Extensions should write all output to this stream, allowing the + * Burp user to configure how that output is handled from within the UI. + * + * @return The extension's standard output stream. + * + * @deprecated Use {@link burp.api.montoya.logging.Logging#logToOutput} instead. + */ + @Deprecated + PrintStream output(); + + /** + * Obtain the current extension's standard error + * stream. Extensions should write all error messages to this stream, + * allowing the Burp user to configure how that output is handled from + * within the UI. + * + * @return The extension's standard error stream. + * + * @deprecated Use {@link burp.api.montoya.logging.Logging#logToError} instead. + */ + @Deprecated + PrintStream error(); + + /** + * This method prints a line of output to the current extension's standard + * output stream. + * + * @param message The message to print. + */ + void logToOutput(String message); + + /** + * This method prints a line of output to the current extension's standard + * error stream. + * + * @param message The message to print. + */ + void logToError(String message); + + /** + * This method prints a message and a stack trace to the current extension's standard + * error stream. + * + * @param message The message to print. + * @param cause The cause of the error being logged. + */ + void logToError(String message, Throwable cause); + + /** + * This method prints a stack trace to the current extension's standard + * error stream. + * + * @param cause The cause of the error being logged. + */ + void logToError(Throwable cause); + + /** + * This method can be used to display a debug event in the Burp Suite + * event log. + * + * @param message The debug message to display. + */ + void raiseDebugEvent(String message); + + /** + * This method can be used to display an informational event in the Burp + * Suite event log. + * + * @param message The informational message to display. + */ + void raiseInfoEvent(String message); + + /** + * This method can be used to display an error event in the Burp Suite + * event log. + * + * @param message The error message to display. + */ + void raiseErrorEvent(String message); + + /** + * This method can be used to display a critical event in the Burp Suite + * event log. + * + * @param message The critical message to display. + */ + void raiseCriticalEvent(String message); +} diff --git a/src/burp/api/montoya/organizer/Organizer.java b/src/burp/api/montoya/organizer/Organizer.java new file mode 100644 index 0000000..e9f9764 --- /dev/null +++ b/src/burp/api/montoya/organizer/Organizer.java @@ -0,0 +1,34 @@ +/* + * Copyright (c) 2022-2023. PortSwigger Ltd. All rights reserved. + * + * This code may be used to extend the functionality of Burp Suite Community Edition + * and Burp Suite Professional, provided that this usage does not violate the + * license terms for those products. + */ + +package burp.api.montoya.organizer; + +import burp.api.montoya.http.message.HttpRequestResponse; +import burp.api.montoya.http.message.requests.HttpRequest; + +/** + * Provides access to the functionality of the Organizer tool. + */ +public interface Organizer +{ + /** + * This method can be used to send an HTTP request to the Burp Organizer + * tool. + * + * @param request The full HTTP request. + */ + void sendToOrganizer(HttpRequest request); + + /** + * This method can be used to send an HTTP request and response to the Burp + * Organizer tool. + * + * @param requestResponse The full HTTP request and response. + */ + void sendToOrganizer(HttpRequestResponse requestResponse); +} diff --git a/src/burp/api/montoya/persistence/PersistedList.java b/src/burp/api/montoya/persistence/PersistedList.java new file mode 100644 index 0000000..c8044be --- /dev/null +++ b/src/burp/api/montoya/persistence/PersistedList.java @@ -0,0 +1,115 @@ +/* + * Copyright (c) 2022-2023. PortSwigger Ltd. All rights reserved. + * + * This code may be used to extend the functionality of Burp Suite Community Edition + * and Burp Suite Professional, provided that this usage does not violate the + * license terms for those products. + */ + +package burp.api.montoya.persistence; + +import burp.api.montoya.core.ByteArray; +import burp.api.montoya.http.message.HttpRequestResponse; +import burp.api.montoya.http.message.requests.HttpRequest; +import burp.api.montoya.http.message.responses.HttpResponse; + +import java.util.List; + +import static burp.api.montoya.internal.ObjectFactoryLocator.FACTORY; + +/** + * List that has been persisted in the project. + * The methods of this list operate on the underlying persisted data. + */ +public interface PersistedList extends List +{ + /** + * Create a new instance of {@link PersistedList} that contains instances of {@link Boolean}. + * + * @return A new {@link PersistedList} instance. + */ + static PersistedList persistedBooleanList() + { + return FACTORY.persistedBooleanList(); + } + + /** + * Create a new instance of {@link PersistedList} that contains instances of {@link Short}. + * + * @return A new {@link PersistedList} instance. + */ + static PersistedList persistedShortList() + { + return FACTORY.persistedShortList(); + } + + /** + * Create a new instance of {@link PersistedList} that contains instances of {@link Integer}. + * + * @return A new {@link PersistedList} instance. + */ + static PersistedList persistedIntegerList() + { + return FACTORY.persistedIntegerList(); + } + + /** + * Create a new instance of {@link PersistedList} that contains instances of {@link Long}. + * + * @return A new {@link PersistedList} instance. + */ + static PersistedList persistedLongList() + { + return FACTORY.persistedLongList(); + } + + /** + * Create a new instance of {@link PersistedList} that contains instances of {@link String}. + * + * @return A new {@link PersistedList} instance. + */ + static PersistedList persistedStringList() + { + return FACTORY.persistedStringList(); + } + + /** + * Create a new instance of {@link PersistedList} that contains instances of {@link ByteArray}. + * + * @return A new {@link PersistedList} instance. + */ + static PersistedList persistedByteArrayList() + { + return FACTORY.persistedByteArrayList(); + } + + /** + * Create a new instance of {@link PersistedList} that contains instances of {@link HttpRequest}. + * + * @return A new {@link PersistedList} instance. + */ + static PersistedList persistedHttpRequestList() + { + return FACTORY.persistedHttpRequestList(); + } + + /** + * Create a new instance of {@link PersistedList} that contains instances of {@link HttpResponse}. + * + * @return A new {@link PersistedList} instance. + */ + static PersistedList persistedHttpResponseList() + { + return FACTORY.persistedHttpResponseList(); + } + + /** + * Create a new instance of {@link PersistedList} that contains instances of {@link HttpRequestResponse}. + * + * @return A new {@link PersistedList} instance. + */ + static PersistedList persistedHttpRequestResponseList() + { + return FACTORY.persistedHttpRequestResponseList(); + } +} diff --git a/src/burp/api/montoya/persistence/PersistedObject.java b/src/burp/api/montoya/persistence/PersistedObject.java new file mode 100644 index 0000000..9c9a3fc --- /dev/null +++ b/src/burp/api/montoya/persistence/PersistedObject.java @@ -0,0 +1,744 @@ +/* + * Copyright (c) 2022-2023. PortSwigger Ltd. All rights reserved. + * + * This code may be used to extend the functionality of Burp Suite Community Edition + * and Burp Suite Professional, provided that this usage does not violate the + * license terms for those products. + */ + +package burp.api.montoya.persistence; + +import burp.api.montoya.core.ByteArray; +import burp.api.montoya.http.message.HttpRequestResponse; +import burp.api.montoya.http.message.requests.HttpRequest; +import burp.api.montoya.http.message.responses.HttpResponse; + +import java.util.Set; + +import static burp.api.montoya.internal.ObjectFactoryLocator.FACTORY; + +/** + * [Professional only] Enables data to be stored and accessed from the Burp project. + * Supports HTTP requests, HTTP responses, byte arrays, primitives, lists of all these, and object hierarchies. + */ +public interface PersistedObject +{ + /** + * {@link PersistedObject} associated with the specified key, + * or {@code null} if this map contains no mapping for the key. + * + * @param key The key whose associated value is to be returned. + * + * @return The value to which the specified key is mapped, or + * {@code null} if this map contains no mapping for the key. + */ + PersistedObject getChildObject(String key); + + /** + * Associates the specified {@link PersistedObject} with the specified key in this map. + * If the map previously contained a mapping for + * the key, the old value is replaced by the specified value. + * + * @param key The key with which the specified child object is to be associated. + * @param childObject The {@link PersistedObject} to be associated with the specified key. + */ + void setChildObject(String key, PersistedObject childObject); + + /** + * Removes the mapping of the specified key to the {@link PersistedObject}. + * + * @param key The key whose mapping is to be deleted. + */ + void deleteChildObject(String key); + + /** + * Retrieve all keys currently mapped for {@link PersistedObject} objects. + * + * @return Set of keys. + */ + Set childObjectKeys(); + + /** + * {@link String} associated with the specified key, + * or {@code null} if this map contains no mapping for the key. + * + * @param key The key whose associated value is to be returned. + * + * @return The value associated with the specified key, or + * {@code null} if this map contains no mapping for the key. + */ + String getString(String key); + + /** + * Associates the specified {@link String} with the specified key in this map. + * This is an optional operation. If the map previously contained a mapping for + * the key, the old value is replaced by the specified value. + * + * @param key The key with which the specified value is to be associated. + * @param value The value to be associated with the specified key. + */ + void setString(String key, String value); + + /** + * Removes the mapping of the specified key to the {@link String}. + * + * @param key The key whose mapping is to be deleted. + */ + void deleteString(String key); + + /** + * Retrieve all keys currently mapped for {@link String} values. + * + * @return Set of keys. + */ + Set stringKeys(); + + /** + * {@link Boolean} associated with the specified key, + * or {@code null} if this map contains no mapping for the key. + * + * @param key The key whose associated value is to be returned. + * + * @return The value associated with the specified key, or + * {@code null} if this map contains no mapping for the key. + */ + Boolean getBoolean(String key); + + /** + * Associates the specified {@code boolean} with the specified key in this map. + * This is an optional operation. If the map previously contained a mapping for + * the key, the old value is replaced by the specified value. + * + * @param key The key with which the specified value is to be associated. + * @param value The value to be associated with the specified key. + * If this value is {@code null} then any value that is currently mapped to the specified key is removed. + */ + void setBoolean(String key, boolean value); + + /** + * Removes the mapping of the specified key to the {@link Boolean}. + * + * @param key The key whose mapping is to be deleted. + */ + void deleteBoolean(String key); + + /** + * Retrieve all keys currently mapped for {@link Boolean} values. + * + * @return Set of keys. + */ + Set booleanKeys(); + + /** + * {@link Byte} associated with the specified key, + * or {@code null} if this map contains no mapping for the key. + * + * @param key The key whose associated value is to be returned. + * + * @return The value associated with the specified key, or + * {@code null} if this map contains no mapping for the key. + */ + Byte getByte(String key); + + /** + * Associates the specified {@code byte} with the specified key in this map + * This is an optional operation. If the map previously contained a mapping for + * the key, the old value is replaced by the specified value. + * + * @param key The key with which the specified value is to be associated. + * @param value The value to be associated with the specified key. + * If this value is {@code null} then any value that is currently mapped to the specified key is removed. + */ + void setByte(String key, byte value); + + /** + * Removes the mapping of the specified key to the {@link Byte}. + * + * @param key The key whose mapping is to be deleted. + */ + void deleteByte(String key); + + /** + * Retrieve all keys currently mapped for {@link Byte} values. + * + * @return Set of keys. + */ + Set byteKeys(); + + /** + * {@link Short} associated with the specified key, + * or {@code null} if this map contains no mapping for the key. + * + * @param key The key whose associated value is to be returned. + * + * @return The value associated with the specified key, or + * {@code null} if this map contains no mapping for the key. + */ + Short getShort(String key); + + /** + * Associates the specified short with the specified key in this map + * This is an optional operation. If the map previously contained a mapping for + * the key, the old value is replaced by the specified value. + * + * @param key The key with which the specified value is to be associated. + * @param value The value to be associated with the specified key. + * If this value is {@code null} then any value currently mapped to the specified key is removed. + */ + void setShort(String key, short value); + + /** + * Removes the mapping from the specified key to the {@link Short}. + * + * @param key The key whose mapping is to be deleted. + */ + void deleteShort(String key); + + /** + * Retrieve all keys currently mapped for {@link Short} values. + * + * @return Set of keys. + */ + Set shortKeys(); + + /** + * {@link Integer} associated with the specified key, + * or {@code null} if this map contains no mapping for the key. + * + * @param key The key whose associated value is to be returned. + * + * @return The value associated with the specified key, or + * {@code null} if this map contains no mapping for the key. + */ + Integer getInteger(String key); + + /** + * Associates the specified {@code int} with the specified key in this map + * This is an optional operation. If the map previously contained a mapping for + * the key, the old value is replaced by the specified value. + * + * @param key The key with which the specified value is to be associated. + * @param value The value to be associated with the specified key. + * If this value is {@code null} then any value that is currently mapped to the specified key is removed. + */ + void setInteger(String key, int value); + + /** + * Removes the mapping from the specified key to the {@link Integer}. + * + * @param key The key whose mapping is to be deleted. + */ + void deleteInteger(String key); + + /** + * Retrieve all keys currently mapped for {@link Integer} values. + * + * @return Set of keys. + */ + Set integerKeys(); + + /** + * {@link Long} associated with the specified key, + * or {@code null}} if this map contains no mapping for the key. + * + * @param key The key whose associated value is to be returned. + * + * @return The value associated with the specified key, or + * {@code null} if this map contains no mapping for the key. + */ + Long getLong(String key); + + /** + * Associates the specified {@code long} with the specified key in this map. + * This is an optional operation. If the map previously contained a mapping for + * the key, the old value is replaced by the specified value. + * + * @param key The key with which the specified value is to be associated. + * @param value The value to be associated with the specified key. + * If this value is {@code null} then any value that is currently mapped to the specified key is removed. + */ + void setLong(String key, long value); + + /** + * Removes the mapping from the specified key to the {@link Long}. + * + * @param key The key whose mapping is to be deleted. + */ + void deleteLong(String key); + + /** + * Retrieve all keys currently mapped for {@link Long} values. + * + * @return Set of keys. + */ + Set longKeys(); + + /** + * {@link ByteArray} associated with the specified key, + * or {@code null} if this map contains no mapping for the key. + * + * @param key The key whose associated value is to be returned. + * + * @return The value associated with the specified key, or + * {@code null} if this map contains no mapping for the key. + */ + ByteArray getByteArray(String key); + + /** + * Associates the specified {@code ByteArray} with the specified key in this map. + * If the map previously contained a mapping for the key, the old value is replaced + * by the specified value. + * + * @param key The key with which the specified value is to be associated. + * @param value The value to be associated with the specified key. + */ + void setByteArray(String key, ByteArray value); + + /** + * Removes the mapping of the specified key to the {@link ByteArray}. + * + * @param key The key whose mapping is to be deleted. + */ + void deleteByteArray(String key); + + /** + * Retrieve all keys currently mapped for {@link ByteArray} values. + * + * @return Set of keys. + */ + Set byteArrayKeys(); + + /** + * {@link HttpRequest} associated with the specified key, + * or {@code null} if this map contains no mapping for the key + * + * @param key The key whose associated value is to be returned. + * + * @return The value associated with the specified key, or + * {@code null} if this map contains no mapping for the key. + */ + HttpRequest getHttpRequest(String key); + + /** + * Associates the specified {@link HttpRequest} with the specified key in this map. + * This is an optional operation. If the map previously contained a mapping for + * the key, the old value is replaced by the specified value. + * + * @param key The key with which the specified value is to be associated. + * @param value The value to be associated with the specified key. + */ + void setHttpRequest(String key, HttpRequest value); + + /** + * Removes the mapping of the specified key to the {@link HttpRequest}. + * + * @param key The key whose mapping is to be deleted. + */ + void deleteHttpRequest(String key); + + /** + * Retrieve all keys currently mapped for {@link HttpRequest} values. + * + * @return Set of keys. + */ + Set httpRequestKeys(); + + /** + * {@link PersistedList} of {@link HttpRequest} associated with the specified key, + * or {@code null} if this map contains no mapping for the key. + * + * @param key The key whose associated value is to be returned. + * + * @return The value associated with the specified key, or + * {@code null} if this map contains no mapping for the key. + */ + PersistedList getHttpRequestList(String key); + + /** + * Associates the specified {@link PersistedList} of {@link HttpRequest} with the specified key in this map. + * If the map previously contained a mapping for the key, + * the old value is replaced by the specified value. + * + * @param key The key with which the specified value is to be associated. + * @param value The value to be associated with the specified key. + * The methods of this list operate on the underlying persisted data. + */ + void setHttpRequestList(String key, PersistedList value); + + /** + * Removes the mapping of the specified key to the {@link PersistedList} of {@link HttpRequest}. + * + * @param key The key whose mapping is to be deleted. + */ + void deleteHttpRequestList(String key); + + /** + * Retrieve all keys currently mapped for {@link HttpRequest} Lists. + * + * @return Set of keys. + */ + Set httpRequestListKeys(); + + /** + * {@link HttpResponse} associated with the specified key, + * or {@code null} if this map contains no mapping for the key + * + * @param key The key whose associated value is to be returned. + * + * @return The value associated with the specified key, or + * {@code null} if this map contains no mapping for the key. + */ + HttpResponse getHttpResponse(String key); + + /** + * Associates the specified {@link HttpResponse} with the specified key in this map. + * This is an optional operation. If the map previously contained a mapping for + * the key, the old value is replaced by the specified value. + * + * @param key The key with which the specified value is to be associated. + * @param value The value to be associated with the specified key. + */ + void setHttpResponse(String key, HttpResponse value); + + /** + * Removes the mapping of the specified key to the {@link HttpResponse}. + * + * @param key The key whose mapping is to be deleted. + */ + void deleteHttpResponse(String key); + + /** + * Retrieve all keys currently mapped for {@link HttpResponse} values. + * + * @return Set of keys. + */ + Set httpResponseKeys(); + + /** + * {@link PersistedList} of {@link HttpResponse} associated with the specified key, + * or {@code null} if this map contains no mapping for the key. + * + * @param key The key whose associated value is to be returned. + * + * @return The value associated with the specified key, or + * {@code null} if this map contains no mapping for the key. + */ + PersistedList getHttpResponseList(String key); + + /** + * Associates the specified {@link PersistedList} of {@link HttpResponse} with the specified key in this map. + * If the map previously contained a mapping for the key, + * the old value is replaced by the specified value. + * + * @param key The key with which the specified value is to be associated. + * @param value The value to be associated with the specified key. + * The methods of this list operate on the underlying persisted data. + */ + void setHttpResponseList(String key, PersistedList value); + + /** + * Removes the mapping of the specified key to the {@link PersistedList} of {@link HttpResponse}. + * + * @param key The key whose mapping is to be deleted. + */ + void deleteHttpResponseList(String key); + + /** + * Retrieve all keys currently mapped for {@link HttpResponse} Lists. + * + * @return Set of keys. + */ + Set httpResponseListKeys(); + + /** + * {@link HttpRequestResponse} associated with the specified key, + * or {@code null} if this map contains no mapping for the key. + * + * @param key The key whose associated value is to be returned. + * + * @return The value associated with the specified key, or + * {@code null} if this map contains no mapping for the key. + */ + HttpRequestResponse getHttpRequestResponse(String key); + + /** + * Associates the specified {@link HttpRequestResponse} with the specified key in this map. + * This is an optional operation. If the map previously contained a mapping for + * the key, the old value is replaced by the specified value. + * + * @param key The key with which the specified value is to be associated. + * @param value The value to be associated with the specified key. + */ + void setHttpRequestResponse(String key, HttpRequestResponse value); + + /** + * Removes the mapping of the specified key to the {@link HttpRequestResponse}. + * + * @param key The key whose mapping is to be deleted. + */ + void deleteHttpRequestResponse(String key); + + /** + * Retrieve all keys currently mapped for {@link HttpRequestResponse} values. + * + * @return Set of keys. + */ + Set httpRequestResponseKeys(); + + /** + * {@link PersistedList} of {@link HttpRequestResponse} associated with the specified key, + * or {@code null} if this map contains no mapping for the key. + * + * @param key The key whose associated value is to be returned. + * + * @return The value associated with the specified key, or + * {@code null} if this map contains no mapping for the key. + */ + PersistedList getHttpRequestResponseList(String key); + + /** + * Associates the specified {@link PersistedList} of {@link HttpRequestResponse} with the specified key in this map. + * If the map previously contained a mapping for the key, + * the old value is replaced by the specified value. + * + * @param key The key with which the specified value is to be associated. + * @param value The value to be associated with the specified key. + * The methods of this list operate on the underlying persisted data. + */ + void setHttpRequestResponseList(String key, PersistedList value); + + /** + * Removes the mapping of the specified key to the {@link PersistedList} of {@link HttpRequestResponse}. + * + * @param key The key whose mapping is to be deleted. + */ + void deleteHttpRequestResponseList(String key); + + /** + * Retrieve all keys currently mapped for {@link HttpRequestResponse} Lists. + * + * @return Set of keys. + */ + Set httpRequestResponseListKeys(); + + /** + * {@link PersistedList} of {@link Boolean} associated with the specified key, + * or {@code null} if this map contains no mapping for the key. + * + * @param key The key whose associated value is to be returned. + * + * @return The value to which the specified key is mapped, or + * {@code null} if this map contains no mapping for the key. + */ + PersistedList getBooleanList(String key); + + /** + * Associates the specified {@link PersistedList} of {@link Boolean} with the specified key in this map. + * If the map previously contained a mapping for the key, + * the old value is replaced by the specified value. + * + * @param key The key with which the specified value is to be associated. + * @param value The value to be associated with the specified key. + */ + void setBooleanList(String key, PersistedList value); + + /** + * Removes the mapping of the specified key to the {@link PersistedList} of {@link Boolean}. + * + * @param key The key whose mapping is to be deleted. + */ + void deleteBooleanList(String key); + + /** + * Retrieve all keys currently mapped for {@link Boolean} Lists. + * + * @return Set of keys. + */ + Set booleanListKeys(); + + /** + * {@link PersistedList} of {@link Short} associated with the specified key, + * or {@code null} if this map contains no mapping for the key. + * + * @param key The key whose associated value is to be returned. + * + * @return The value to which the specified key is mapped, or + * {@code null} if this map contains no mapping for the key. + */ + PersistedList getShortList(String key); + + /** + * Associates the specified {@link PersistedList} of {@link Short} with the specified key in this map. + * If the map previously contained a mapping for the key, + * the old value is replaced by the specified value. + * + * @param key The key with which the specified value is to be associated. + * @param value The value to be associated with the specified key. + */ + void setShortList(String key, PersistedList value); + + /** + * Removes the mapping of the specified key to the {@link PersistedList} of {@link Short}. + * + * @param key The key whose mapping is to be deleted. + */ + void deleteShortList(String key); + + /** + * Retrieve all keys currently mapped for {@link Short} Lists. + * + * @return Set of keys. + */ + Set shortListKeys(); + + /** + * {@link PersistedList} of {@link Integer} associated with the specified key, + * or {@code null} if this map contains no mapping for the key. + * + * @param key The key whose associated value is to be returned. + * + * @return The value to which the specified key is mapped, or + * {@code null} if this map contains no mapping for the key. + */ + PersistedList getIntegerList(String key); + + /** + * Associates the specified {@link PersistedList} of {@link Integer} with the specified key in this map. + * If the map previously contained a mapping for the key, + * the old value is replaced by the specified value. + * + * @param key The key with which the specified value is to be associated. + * @param value The value to be associated with the specified key. + */ + void setIntegerList(String key, PersistedList value); + + /** + * Removes the mapping of the specified key to the {@link PersistedList} of {@link Integer}. + * + * @param key The key whose mapping is to be deleted. + */ + void deleteIntegerList(String key); + + /** + * Retrieve all keys currently mapped for {@link Integer} Lists. + * + * @return Set of keys. + */ + Set integerListKeys(); + + /** + * {@link PersistedList} of {@link Long} associated with the specified key, + * or {@code null} if this map contains no mapping for the key. + * + * @param key The key whose associated value is to be returned. + * + * @return The value to which the specified key is mapped, or + * {@code null} if this map contains no mapping for the key. + */ + PersistedList getLongList(String key); + + /** + * Associates the specified {@link PersistedList} of {@link Long} with the specified key in this map. + * If the map previously contained a mapping for the key, + * the old value is replaced by the specified value. + * + * @param key The key with which the specified value is to be associated. + * @param value The value to be associated with the specified key. + */ + void setLongList(String key, PersistedList value); + + /** + * Removes the mapping of the specified key to the {@link PersistedList} of {@link Long}. + * + * @param key The key whose mapping is to be deleted. + */ + void deleteLongList(String key); + + /** + * Retrieve all keys currently mapped for {@link Long} Lists. + * + * @return Set of keys. + */ + Set longListKeys(); + + /** + * {@link PersistedList} of {@link String} associated with the specified key, + * or {@code null} if this map contains no mapping for the key. + * + * @param key The key whose associated value is to be returned. + * + * @return The value to which the specified key is mapped, or + * {@code null} if this map contains no mapping for the key. + */ + PersistedList getStringList(String key); + + /** + * Associates the specified {@link PersistedList} of {@link String} with the specified key in this map. + * If the map previously contained a mapping for the key, + * the old value is replaced by the specified value. + * + * @param key The key with which the specified value is to be associated. + * @param value The value to be associated with the specified key. + */ + void setStringList(String key, PersistedList value); + + /** + * Removes the mapping of the specified key to the {@link PersistedList} of {@link String}. + * + * @param key The key whose mapping is to be deleted. + */ + void deleteStringList(String key); + + /** + * Retrieve all keys currently mapped for {@link String} Lists. + * + * @return Set of keys. + */ + Set stringListKeys(); + + /** + * {@link PersistedList} of {@link ByteArray} associated with the specified key, + * or {@code null} if this map contains no mapping for the key. + * + * @param key The key whose associated value is to be returned. + * + * @return The value associated with the specified key, or + * {@code null} if this map contains no mapping for the key. + */ + PersistedList getByteArrayList(String key); + + /** + * Associates the specified {@link PersistedList} of {@code ByteArray} with the specified key in this map. + * If the map previously contained a mapping for the key, + * the old value is replaced by the specified value. + * + * @param key The key with which the specified value is to be associated. + * @param value The value to be associated with the specified key. + * The methods of this list operate on the underlying persisted data. + */ + void setByteArrayList(String key, PersistedList value); + + /** + * Removes the mapping of the specified key to the {@link PersistedList} of {@link ByteArray}. + * + * @param key The key whose mapping is to be deleted. + */ + void deleteByteArrayList(String key); + + /** + * Retrieve all keys currently mapped for {@link ByteArray} Lists. + * + * @return Set of keys. + */ + Set byteArrayListKeys(); + + /** + * Create a new instance of {@link PersistedObject}. + * + * @return A new {@link PersistedObject} instance. + */ + static PersistedObject persistedObject() + { + return FACTORY.persistedObject(); + } +} diff --git a/src/burp/api/montoya/persistence/Persistence.java b/src/burp/api/montoya/persistence/Persistence.java new file mode 100644 index 0000000..558522e --- /dev/null +++ b/src/burp/api/montoya/persistence/Persistence.java @@ -0,0 +1,33 @@ +/* + * Copyright (c) 2022-2023. PortSwigger Ltd. All rights reserved. + * + * This code may be used to extend the functionality of Burp Suite Community Edition + * and Burp Suite Professional, provided that this usage does not violate the + * license terms for those products. + */ + +package burp.api.montoya.persistence; + +/** + * Provides access to the persistence functionality. + */ +public interface Persistence +{ + /** + * Access data storage functionality in the Burp project. When Burp is started without + * a project file, the data is stored in memory. + * + * @return An implementation of the {@link PersistedObject} interface + * that stores data in either the project file or memory. + */ + PersistedObject extensionData(); + + /** + * Access Java preference store functionality + * in a way that survives reloads of the extension and of Burp Suite. + * + * @return An implementation of the {@link Preferences} interface + * that stores data in a persistent way. + */ + Preferences preferences(); +} diff --git a/src/burp/api/montoya/persistence/Preferences.java b/src/burp/api/montoya/persistence/Preferences.java new file mode 100644 index 0000000..2dcd4c4 --- /dev/null +++ b/src/burp/api/montoya/persistence/Preferences.java @@ -0,0 +1,232 @@ +/* + * Copyright (c) 2022-2023. PortSwigger Ltd. All rights reserved. + * + * This code may be used to extend the functionality of Burp Suite Community Edition + * and Burp Suite Professional, provided that this usage does not violate the + * license terms for those products. + */ + +package burp.api.montoya.persistence; + +import java.util.Set; + +/** + * Enables data to be stored and accessed from the Java preference store. Supports primitives. + */ +public interface Preferences +{ + /** + * {@link String} associated with the specified key. + * Returns {@code null} if this map contains no mapping for the key. + * + * @param key The key whose associated value is to be returned. + * + * @return The value associated with the specified key, or + * {@code null} if this map contains no mapping for the key. + */ + String getString(String key); + + /** + * Associates the specified {@link String} with the specified key in this map. + * This is an optional operation. If the map previously contained a mapping for + * the key, the old value is replaced by the specified value. + * + * @param key The key with which the specified value is to be associated. + * @param value The value to be associated with the specified key. + */ + void setString(String key, String value); + + /** + * Removes the mapping from the specified key to the {@link String}. + * + * @param key The key whose mapping is to be deleted. + */ + void deleteString(String key); + + /** + * Retrieve all keys currently mapped for {@link String} values. + * + * @return Set of keys. + */ + Set stringKeys(); + + /** + * {@link Boolean} associated with the specified key. + * Returns {@code null} if this map contains no mapping for the key. + * + * @param key The key whose associated value is to be returned. + * + * @return The value associated with the specified key, or + * {@code null} if this map contains no mapping for the key. + */ + Boolean getBoolean(String key); + + /** + * Associates the specified {@code boolean} with the specified key in this map. + * This is an optional operation. If the map previously contained a mapping for + * the key, the old value is replaced by the specified value. + * + * @param key The key with which the specified value is to be associated. + * @param value The value to be associated with the specified key. + * If this value is {@code null} then any value that is currently mapped to the specified key is removed. + */ + void setBoolean(String key, boolean value); + + /** + * Removes the mapping from the specified key to the {@link Boolean}. + * + * @param key The key whose mapping is to be deleted. + */ + void deleteBoolean(String key); + + /** + * Retrieve all keys currently mapped for {@link Boolean} values. + * + * @return Set of keys. + */ + Set booleanKeys(); + + /** + * {@link Byte} associated with the specified key. + * Returns {@code null} if this map contains no mapping for the key. + * + * @param key The key whose associated value is to be returned. + * + * @return The value associated with the specified key, or + * {@code null} if this map contains no mapping for the key. + */ + Byte getByte(String key); + + /** + * Associates the specified {@code byte} with the specified key in this map. + * This is an optional operation. If the map previously contained a mapping for + * the key, the old value is replaced by the specified value. + * + * @param key The key with which the specified value is to be associated. + * @param value The value to be associated with the specified key. + * If this value is {@code null} then any value that is currently mapped to the specified key is removed. + */ + void setByte(String key, byte value); + + /** + * Removes the mapping from the specified key to the {@link Byte}. + * + * @param key The key whose mapping is to be deleted. + */ + void deleteByte(String key); + + /** + * Retrieve all keys currently mapped for {@link Byte} values. + * + * @return Set of keys. + */ + Set byteKeys(); + + /** + * {@link Short} associated with the specified key. + * Returns {@code null} if this map contains no mapping for the key. + * + * @param key The key whose associated value is to be returned. + * + * @return The value associated with the specified key, or + * {@code null} if this map contains no mapping for the key. + */ + Short getShort(String key); + + /** + * Associates the specified short with the specified key in this map. + * This is an optional operation. If the map previously contained a mapping for + * the key, the old value is replaced by the specified value. + * + * @param key The key with which the specified value is to be associated. + * @param value The value to be associated with the specified key. + * If this value is {@code null} then any value that is currently mapped to the specified key is removed. + */ + void setShort(String key, short value); + + /** + * Removes the mapping from the specified key to the {@link Short}. + * + * @param key The key whose mapping is to be deleted. + */ + void deleteShort(String key); + + /** + * Retrieve all keys currently mapped for {@link Short} values. + * + * @return Set of keys. + */ + Set shortKeys(); + + /** + * {@link Integer} associated with the specified key. + * Returns {@code null} if this map contains no mapping for the key. + * + * @param key The key whose associated value is to be returned. + * + * @return The value associated with the specified key, or + * {@code null} if this map contains no mapping for the key. + */ + Integer getInteger(String key); + + /** + * Associates the specified {@code int} with the specified key in this map. + * This is an optional operation. If the map previously contained a mapping for + * the key, the old value is replaced by the specified value. + * + * @param key The key with which the specified value is to be associated. + * @param value The value to be associated with the specified key. + * If this value is {@code null} then any value that is currently mapped to the specified key is removed. + */ + void setInteger(String key, int value); + + /** + * Removes the mapping from the specified key to the {@link Integer}. + * + * @param key The key whose mapping is to be deleted. + */ + void deleteInteger(String key); + + /** + * Retrieve all keys currently mapped for {@link Integer} values. + * + * @return Set of keys. + */ + Set integerKeys(); + + /** + * {@link Long} associated with the specified key, + * or {@code null}} if this map contains no mapping for the key. + * + * @param key The key whose associated value is to be returned. + * + * @return The value associated with the specified key, or + * {@code null} if this map contains no mapping for the key. + */ + Long getLong(String key); + + /** + * Associates the specified {@code long} with the specified key in this map. + * This is an optional operation. If the map previously contained a mapping for + * the key, the old value is replaced by the specified value. + * + * @param key The key with which the specified value is to be associated. + * @param value The value to be associated with the specified key. + * If this value is {@code null} then any value currently mapped to the specified key is removed. + */ + void setLong(String key, long value); + + /** + * Removes the mapping from the specified key to the {@link Long}. + * + * @param key The key whose mapping is to be deleted. + */ + void deleteLong(String key); + + /** + * Retrieve all keys currently mapped for {@link Long} values. + * + * @return Set of keys. + */ + Set longKeys(); +} diff --git a/src/burp/api/montoya/proxy/MessageReceivedAction.java b/src/burp/api/montoya/proxy/MessageReceivedAction.java new file mode 100644 index 0000000..6930fb9 --- /dev/null +++ b/src/burp/api/montoya/proxy/MessageReceivedAction.java @@ -0,0 +1,39 @@ +/* + * Copyright (c) 2022-2023. PortSwigger Ltd. All rights reserved. + * + * This code may be used to extend the functionality of Burp Suite Community Edition + * and Burp Suite Professional, provided that this usage does not violate the + * license terms for those products. + */ + +package burp.api.montoya.proxy; + +/** + * This enum represents the initial action to be taken when intercepting HTTP and WebSocket + * messages in the Proxy. + */ +public enum MessageReceivedAction +{ + /** + * Causes Burp Proxy to follow the current interception rules to determine + * the appropriate action to take for the message. + */ + CONTINUE, + + /** + * Causes Burp Proxy to present the message to the user for manual review + * or modification. + */ + INTERCEPT, + + /** + * Causes Burp Proxy to forward the message without presenting it to the + * user. + */ + DO_NOT_INTERCEPT, + + /** + * Causes Burp Proxy to drop the message. + */ + DROP +} diff --git a/src/burp/api/montoya/proxy/MessageToBeSentAction.java b/src/burp/api/montoya/proxy/MessageToBeSentAction.java new file mode 100644 index 0000000..16a8609 --- /dev/null +++ b/src/burp/api/montoya/proxy/MessageToBeSentAction.java @@ -0,0 +1,26 @@ +/* + * Copyright (c) 2022-2023. PortSwigger Ltd. All rights reserved. + * + * This code may be used to extend the functionality of Burp Suite Community Edition + * and Burp Suite Professional, provided that this usage does not violate the + * license terms for those products. + */ + +package burp.api.montoya.proxy; + +/** + * This enum represents the final action to be taken when intercepting HTTP and WebSocket + * messages in the Proxy. + */ +public enum MessageToBeSentAction +{ + /** + * Causes Burp Proxy to forward the message. + */ + CONTINUE, + + /** + * Causes Burp Proxy to drop the message. + */ + DROP +} diff --git a/src/burp/api/montoya/proxy/Proxy.java b/src/burp/api/montoya/proxy/Proxy.java new file mode 100644 index 0000000..6823fc3 --- /dev/null +++ b/src/burp/api/montoya/proxy/Proxy.java @@ -0,0 +1,107 @@ +/* + * Copyright (c) 2022-2023. PortSwigger Ltd. All rights reserved. + * + * This code may be used to extend the functionality of Burp Suite Community Edition + * and Burp Suite Professional, provided that this usage does not violate the + * license terms for those products. + */ + +package burp.api.montoya.proxy; + +import burp.api.montoya.core.Registration; +import burp.api.montoya.proxy.http.ProxyRequestHandler; +import burp.api.montoya.proxy.http.ProxyResponseHandler; +import burp.api.montoya.proxy.websocket.ProxyWebSocketCreationHandler; + +import java.util.List; + +/** + * Provides access to the functionality of the Proxy tool. + */ +public interface Proxy +{ + /** + * This method enables the master interception for Burp Proxy. + */ + void enableIntercept(); + + /** + * This method disables the master interception for Burp Proxy. + */ + void disableIntercept(); + + /** + * This method returns details of all items in the Proxy HTTP history. + * + * @return The list of all the {@link ProxyHttpRequestResponse} items in the + * Proxy HTTP history. + */ + List history(); + + /** + * This method returns details of items in the Proxy HTTP history based on + * the filter. + * + * @param filter An instance of {@link ProxyHistoryFilter} that can be used + * to filter the items in the Proxy history. + * + * @return The list of {@link ProxyHttpRequestResponse} items in the Proxy + * HTTP history that matched the filter. + */ + List history(ProxyHistoryFilter filter); + + /** + * This method returns details of all items in the Proxy WebSockets history. + * + * @return The list of all the {@link ProxyWebSocketMessage} items in the + * Proxy WebSockets history. + */ + List webSocketHistory(); + + /** + * This method returns details of items in the Proxy WebSockets history based + * on the filter. + * + * @param filter An instance of {@link ProxyWebSocketHistoryFilter} that can be used + * to filter the items in the Proxy WebSockets history. + * + * @return The list of {@link ProxyWebSocketMessage} items in the Proxy WebSockets + * history that matched the filter. + */ + List webSocketHistory(ProxyWebSocketHistoryFilter filter); + + /** + * Register a handler which will be notified of + * requests being processed by the Proxy tool. Extensions can perform + * custom analysis or modification of these messages, and control in-UI + * message interception. + * + * @param handler An object created by the extension that implements the + * {@link ProxyRequestHandler} interface. + * + * @return The {@link Registration} for the handler. + */ + Registration registerRequestHandler(ProxyRequestHandler handler); + + /** + * Register a handler which will be notified of + * responses being processed by the Proxy tool. Extensions can perform + * custom analysis or modification of these messages, and control in-UI + * message interception. + * + * @param handler An object created by the extension that implements the + * {@link ProxyResponseHandler} interface. + * + * @return The {@link Registration} for the handler. + */ + Registration registerResponseHandler(ProxyResponseHandler handler); + + /** + * Register a handler which will be invoked whenever a WebSocket is being created by the Proxy tool. + * + * @param handler An object created by the extension that implements {@link ProxyWebSocketCreationHandler} interface. + * + * @return The {@link Registration} for the handler. + */ + Registration registerWebSocketCreationHandler(ProxyWebSocketCreationHandler handler); +} diff --git a/src/burp/api/montoya/proxy/ProxyHistoryFilter.java b/src/burp/api/montoya/proxy/ProxyHistoryFilter.java new file mode 100644 index 0000000..28dcde3 --- /dev/null +++ b/src/burp/api/montoya/proxy/ProxyHistoryFilter.java @@ -0,0 +1,30 @@ +/* + * Copyright (c) 2022-2023. PortSwigger Ltd. All rights reserved. + * + * This code may be used to extend the functionality of Burp Suite Community Edition + * and Burp Suite Professional, provided that this usage does not violate the + * license terms for those products. + */ + +package burp.api.montoya.proxy; + +/** + * Extensions can implement this interface and then call + * {@link Proxy#history(ProxyHistoryFilter)} to get a filtered list of items in + * the Proxy history. + */ +public interface ProxyHistoryFilter +{ + /** + * This method is invoked for every item in the Proxy history to determine + * whether it should be included in the filtered list of items. + * + * @param requestResponse A {@link ProxyHttpRequestResponse} object that + * extensions can use to determine whether the item should be included in + * the filtered list of items. + * + * @return Return {@code true} if the item should be included in the + * filtered list of items. + */ + boolean matches(ProxyHttpRequestResponse requestResponse); +} diff --git a/src/burp/api/montoya/proxy/ProxyHttpRequestResponse.java b/src/burp/api/montoya/proxy/ProxyHttpRequestResponse.java new file mode 100644 index 0000000..fd77ca0 --- /dev/null +++ b/src/burp/api/montoya/proxy/ProxyHttpRequestResponse.java @@ -0,0 +1,207 @@ +/* + * Copyright (c) 2022-2023. PortSwigger Ltd. All rights reserved. + * + * This code may be used to extend the functionality of Burp Suite Community Edition + * and Burp Suite Professional, provided that this usage does not violate the + * license terms for those products. + */ + +package burp.api.montoya.proxy; + +import burp.api.montoya.core.Annotations; +import burp.api.montoya.http.HttpService; +import burp.api.montoya.http.handler.TimingData; +import burp.api.montoya.http.message.MimeType; +import burp.api.montoya.http.message.requests.HttpRequest; +import burp.api.montoya.http.message.requests.MalformedRequestException; +import burp.api.montoya.http.message.responses.HttpResponse; + +import java.time.ZonedDateTime; +import java.util.regex.Pattern; + +/** + * HTTP request and response intercepted by the Proxy. + */ +public interface ProxyHttpRequestResponse +{ + /** + * This method retrieves the HTTP request that was sent by Burp Proxy. + * + * @return The {@link HttpRequest} that was sent by Burp Proxy. + * @see ProxyHttpRequestResponse#finalRequest() + */ + HttpRequest request(); + + /** + * This method retrieves the HTTP request that was sent by Burp Proxy. + * + * @return The {@link HttpRequest} that was sent by Burp Proxy. + */ + HttpRequest finalRequest(); + + /** + * This method retrieves the HTTP response that was received by Burp Proxy. + * + * @return The {@link HttpResponse} that was received by Burp Proxy. + * @see ProxyHttpRequestResponse#originalResponse() + */ + HttpResponse response(); + + /** + * This method retrieves the HTTP response that was received by Burp Proxy. + * + * @return The {@link HttpResponse} that was received by Burp Proxy. + */ + HttpResponse originalResponse(); + + /** + * This method retrieves the annotations for the request/response pair. + * + * @return The {@link Annotations} for the request/response pair. + */ + Annotations annotations(); + + /** + * HTTP service for the request. + * + * @return An {@link HttpService} object containing details of the HTTP service. + */ + HttpService httpService(); + + /** + * URL for the issued final request. + * If the request is malformed, then a {@link MalformedRequestException} is thrown. + * + * @return The URL in the request. + * @throws MalformedRequestException if request is malformed. + */ + @Deprecated(forRemoval = true) + String url(); + + /** + * HTTP method for the issued final request. + * If the request is malformed, then a {@link MalformedRequestException} is thrown. + * + * @return The HTTP method used in the request. + * @throws MalformedRequestException if request is malformed. + * @deprecated use {@link #finalRequest()} method instead. + */ + @Deprecated(forRemoval = true) + String method(); + + /** + * Path and File for the issued final request. + * If the request is malformed, then a {@link MalformedRequestException} is thrown. + * + * @return the path and file in the request + * @throws MalformedRequestException if request is malformed. + * @deprecated use {@link #finalRequest()} path instead. + */ + @Deprecated(forRemoval = true) + String path(); + + /** + * @return The hostname or IP address for the service. + * @deprecated use {@link #finalRequest()} httpService instead. + */ + @Deprecated(forRemoval = true) + String host(); + + /** + * @return The port number for the service. + * @deprecated use {@link #finalRequest()} httpService instead. + */ + @Deprecated(forRemoval = true) + int port(); + + /** + * @return True is a secure protocol is used for the connection, false otherwise. + * @deprecated use {@link #finalRequest()} httpService instead. + */ + @Deprecated(forRemoval = true) + boolean secure(); + + /** + * @return The {@code String} representation of the service. + * @deprecated use {@link #finalRequest()} httpService instead. + */ + @Deprecated(forRemoval = true) + String httpServiceString(); + + /** + * HTTP Version text parsed from the request line for HTTP 1 messages. + * HTTP 2 messages will return "HTTP/2" + * + * @return Version string + * @deprecated use {@link #finalRequest()} httpVersion instead. + */ + @Deprecated(forRemoval = true) + String requestHttpVersion(); + + /** + * Body of the issued final request + * + * @return The body of a message as a {@code String}. + * @deprecated use {@link #finalRequest()} body instead. + */ + @Deprecated(forRemoval = true) + String requestBody(); + + /** + * @return True if the request or response was edited + */ + boolean edited(); + + /** + * Returns the date and time at which Burp Proxy received the request. + * + * @return The time at which Burp Proxy received the request. + */ + ZonedDateTime time(); + + /** + * Returns the proxy listener port used for the request/response. + * + * @return the port number used by the proxy listener + */ + int listenerPort(); + + /** + * Obtain the MIME type of the response or request, as determined by Burp Suite. + * If there is no response the mime type will be determined from the request url. + * + * @return The MIME type. + */ + MimeType mimeType(); + + /** + * @return True if there is a response. + */ + boolean hasResponse(); + + /** + * Searches the data in the HTTP request and response for the specified search term. + * + * @param searchTerm The value to be searched for. + * @param caseSensitive Flags whether the search is case-sensitive. + * + * @return True if the search term is found. + */ + boolean contains(String searchTerm, boolean caseSensitive); + + /** + * Searches the data in the HTTP request and response for the specified regular expression. + * + * @param pattern The regular expression to be searched for. + * + * @return True if the pattern is matched. + */ + boolean contains(Pattern pattern); + + /** + * Retrieve the timing data associated with this request and response. + * + * @return The timing data. + */ + TimingData timingData(); +} diff --git a/src/burp/api/montoya/proxy/ProxyWebSocketHistoryFilter.java b/src/burp/api/montoya/proxy/ProxyWebSocketHistoryFilter.java new file mode 100644 index 0000000..ab41943 --- /dev/null +++ b/src/burp/api/montoya/proxy/ProxyWebSocketHistoryFilter.java @@ -0,0 +1,30 @@ +/* + * Copyright (c) 2022-2023. PortSwigger Ltd. All rights reserved. + * + * This code may be used to extend the functionality of Burp Suite Community Edition + * and Burp Suite Professional, provided that this usage does not violate the + * license terms for those products. + */ + +package burp.api.montoya.proxy; + +/** + * Extensions can implement this interface and then call + * {@link Proxy#webSocketHistory(ProxyWebSocketHistoryFilter)} to get a filtered list of items in + * the Proxy WebSockets history. + */ +public interface ProxyWebSocketHistoryFilter +{ + /** + * This method is invoked for every item in the Proxy WebSockets history to determine + * whether it should be included in the filtered list of items. + * + * @param message A {@link ProxyWebSocketMessage} object that + * extensions can use to determine whether the item should be included in + * the filtered list of items. + * + * @return Return {@code true} if the item should be included in the + * filtered list of items. + */ + boolean matches(ProxyWebSocketMessage message); +} diff --git a/src/burp/api/montoya/proxy/ProxyWebSocketMessage.java b/src/burp/api/montoya/proxy/ProxyWebSocketMessage.java new file mode 100644 index 0000000..b71d4d6 --- /dev/null +++ b/src/burp/api/montoya/proxy/ProxyWebSocketMessage.java @@ -0,0 +1,91 @@ +/* + * Copyright (c) 2022-2023. PortSwigger Ltd. All rights reserved. + * + * This code may be used to extend the functionality of Burp Suite Community Edition + * and Burp Suite Professional, provided that this usage does not violate the + * license terms for those products. + */ + +package burp.api.montoya.proxy; + +import burp.api.montoya.core.Annotations; +import burp.api.montoya.core.ByteArray; +import burp.api.montoya.http.message.requests.HttpRequest; +import burp.api.montoya.ui.contextmenu.WebSocketMessage; +import burp.api.montoya.websocket.Direction; + +import java.time.ZonedDateTime; +import java.util.regex.Pattern; + +/** + * WebSocket message intercepted by the Proxy. + */ +public interface ProxyWebSocketMessage extends WebSocketMessage +{ + /** + * This method retrieves the annotations for the message. + * + * @return The {@link Annotations} for the message. + */ + @Override + Annotations annotations(); + + /** + * @return The direction of the message. + */ + @Override + Direction direction(); + + /** + * @return WebSocket payload. + */ + @Override + ByteArray payload(); + + /** + * @return The {@link HttpRequest} used to create the WebSocket. + */ + @Override + HttpRequest upgradeRequest(); + + /** + * @return The ID for the web socket connection that this message is linked to. + */ + int webSocketId(); + + /** + * @return An instance of {@link ZonedDateTime} indicating when the message was sent. + */ + ZonedDateTime time(); + + /** + * @return The payload after modification from tools and extensions. {@code null} if the message has not been edited. + */ + ByteArray editedPayload(); + + /** + * Returns the proxy listener port used for the web socket message. + * + * @return the port number used by the proxy listener + */ + int listenerPort(); + + /** + * Searches the data in the web socket message for the specified search term. + * + * @param searchTerm The value to be searched for. + * @param caseSensitive Flags whether the search is case-sensitive. + * + * @return True if the search term is found. + */ + boolean contains(String searchTerm, boolean caseSensitive); + + /** + * Searches the data in the web socket message for the specified regular expression. + * + * @param pattern The regular expression to be searched for. + * + * @return True if the pattern is matched. + */ + boolean contains(Pattern pattern); +} \ No newline at end of file diff --git a/src/burp/api/montoya/proxy/http/InterceptedHttpMessage.java b/src/burp/api/montoya/proxy/http/InterceptedHttpMessage.java new file mode 100644 index 0000000..57384ff --- /dev/null +++ b/src/burp/api/montoya/proxy/http/InterceptedHttpMessage.java @@ -0,0 +1,52 @@ +/* + * Copyright (c) 2022-2023. PortSwigger Ltd. All rights reserved. + * + * This code may be used to extend the functionality of Burp Suite Community Edition + * and Burp Suite Professional, provided that this usage does not violate the + * license terms for those products. + */ + +package burp.api.montoya.proxy.http; + +import java.net.InetAddress; + +/** + * HTTP message intercepted by Burp Proxy. + */ +public interface InterceptedHttpMessage +{ + /** + * This method retrieves a unique ID for this request/response. + * + * @return An identifier that is unique to a single request/response pair. + * Extensions can use this to correlate details of requests and responses + * and perform processing on the response message accordingly. + */ + int messageId(); + + /** + * This method retrieves the name of the Burp Proxy listener that is + * processing the intercepted message. + * + * @return The name of the Burp Proxy listener that is processing the + * intercepted message. The format is the same as that shown in the Proxy + * Listeners UI - for example, "127.0.0.1:8080". + */ + String listenerInterface(); + + /** + * This method retrieves the IP address for the source of the intercepted + * message. + * + * @return The IP address for the source of the intercepted message. + */ + InetAddress sourceIpAddress(); + + /** + * This method retrieves the IP address for the destination of the + * intercepted message. + * + * @return The IP address for the destination of the intercepted message. + */ + InetAddress destinationIpAddress(); +} diff --git a/src/burp/api/montoya/proxy/http/InterceptedRequest.java b/src/burp/api/montoya/proxy/http/InterceptedRequest.java new file mode 100644 index 0000000..e842ecb --- /dev/null +++ b/src/burp/api/montoya/proxy/http/InterceptedRequest.java @@ -0,0 +1,583 @@ +/* + * Copyright (c) 2022-2023. PortSwigger Ltd. All rights reserved. + * + * This code may be used to extend the functionality of Burp Suite Community Edition + * and Burp Suite Professional, provided that this usage does not violate the + * license terms for those products. + */ + +package burp.api.montoya.proxy.http; + +import burp.api.montoya.core.Annotations; +import burp.api.montoya.core.ByteArray; +import burp.api.montoya.core.Marker; +import burp.api.montoya.http.HttpService; +import burp.api.montoya.http.message.ContentType; +import burp.api.montoya.http.message.HttpHeader; +import burp.api.montoya.http.message.params.HttpParameter; +import burp.api.montoya.http.message.params.HttpParameterType; +import burp.api.montoya.http.message.params.ParsedHttpParameter; +import burp.api.montoya.http.message.requests.HttpRequest; +import burp.api.montoya.http.message.requests.HttpTransformation; +import burp.api.montoya.http.message.requests.MalformedRequestException; + +import java.net.InetAddress; +import java.util.List; +import java.util.regex.Pattern; + +/** + * HTTP request intercepted by Burp Proxy. + */ +public interface InterceptedRequest extends InterceptedHttpMessage, HttpRequest +{ + /** + * @return Annotations for request/response. + */ + Annotations annotations(); + + /** + * @return True if the request is in-scope. + */ + @Override + boolean isInScope(); + + /** + * HTTP service for the request. + * + * @return An {@link HttpService} object containing details of the HTTP service. + */ + @Override + HttpService httpService(); + + /** + * URL for the request. + * If the request is malformed, then a {@link MalformedRequestException} is thrown. + * + * @return The URL in the request. + * @throws MalformedRequestException if request is malformed. + */ + @Override + String url(); + + /** + * HTTP method for the request. + * If the request is malformed, then a {@link MalformedRequestException} is thrown. + * + * @return The HTTP method used in the request. + * @throws MalformedRequestException if request is malformed. + */ + @Override + String method(); + + /** + * Request path including the query parameters. + * If the request is malformed, then a {@link MalformedRequestException} is thrown. + * + * @return the path and query parameters. + * @throws MalformedRequestException if request is malformed. + */ + @Override + String path(); + + /** + * Request path excluding the query parameters. + * If the request is malformed, then a {@link MalformedRequestException} is thrown. + * + * @return the path excluding query parameters. + * @throws MalformedRequestException if request is malformed. + */ + @Override + String pathWithoutQuery(); + + /** + * HTTP Version text parsed from the request line for HTTP 1 messages. + * HTTP 2 messages will return "HTTP/2" + * + * @return Version string + */ + @Override + String httpVersion(); + + /** + * HTTP headers contained in the message. + * + * @return A list of HTTP headers. + */ + @Override + List headers(); + + /** + * @param header The header to check if it exists in the request. + * + * @return True if the header exists in the request. + */ + @Override + boolean hasHeader(HttpHeader header); + + /** + * @param name The name of the header to query within the request. + * + * @return True if a header exists in the request with the supplied name. + */ + @Override + boolean hasHeader(String name); + + /** + * @param name The name of the header to check. + * @param value The value of the header to check. + * + * @return True if a header exists in the request that matches the name and value supplied. + */ + @Override + boolean hasHeader(String name, String value); + + /** + * @param name The name of the header to retrieve. + * + * @return An instance of {@link HttpHeader} that matches the name supplied, {@code null} if no match found. + */ + @Override + HttpHeader header(String name); + + /** + * @param name The name of the header to retrieve. + * + * @return The {@code String} value of the header that matches the name supplied, {@code null} if no match found. + */ + @Override + String headerValue(String name); + + /** + * @return True if the request has parameters. + */ + @Override + boolean hasParameters(); + + /** + * @return True if the request has parameters of type {@link HttpParameterType} + */ + @Override + boolean hasParameters(HttpParameterType type); + + /** + * @param name The name of the parameter to find. + * @param type The type of the parameter to find. + * + * @return An instance of {@link ParsedHttpParameter} that matches the type and name specified. {@code null} if not found. + */ + @Override + ParsedHttpParameter parameter(String name, HttpParameterType type); + + /** + * @param name The name of the parameter to get the value from. + * @param type The type of the parameter to get the value from. + * + * @return The value of the parameter that matches the name and type specified. {@code null} if not found. + */ + @Override + String parameterValue(String name, HttpParameterType type); + + /** + * @param name The name of the parameter to find. + * @param type The type of the parameter to find. + * + * @return {@code true} if a parameter exists that matches the name and type specified. {@code false} if not found. + */ + @Override + boolean hasParameter(String name, HttpParameterType type); + + /** + * @param parameter An instance of {@link HttpParameter} to match to an existing parameter. + * + * @return {@code true} if a parameter exists that matches the data within the provided {@link HttpParameter}. {@code false} if not found. + */ + @Override + boolean hasParameter(HttpParameter parameter); + + /** + * @return The detected content type of the request. + */ + @Override + ContentType contentType(); + + /** + * @return The parameters contained in the request. + */ + @Override + List parameters(); + + /** + * @param type The type of parameter that will be returned in the filtered list. + * + * @return A filtered list of {@link ParsedHttpParameter} containing only the provided type. + */ + @Override + List parameters(HttpParameterType type); + + /** + * Body of a message as a byte array. + * + * @return The body of a message as a byte array. + */ + @Override + ByteArray body(); + + /** + * Body of a message as a {@code String}. + * + * @return The body of a message as a {@code String}. + */ + @Override + String bodyToString(); + + /** + * Offset within the message where the message body begins. + * + * @return The message body offset. + */ + @Override + int bodyOffset(); + + /** + * Markers for the message. + * + * @return A list of markers. + */ + @Override + List markers(); + + /** + * Searches the data in the HTTP message for the specified search term. + * + * @param searchTerm The value to be searched for. + * @param caseSensitive Flags whether the search is case-sensitive. + * + * @return True if the search term is found. + */ + @Override + boolean contains(String searchTerm, boolean caseSensitive); + + /** + * Searches the data in the HTTP message for the specified regular expression. + * + * @param pattern The regular expression to be searched for. + * + * @return True if the pattern is matched. + */ + @Override + boolean contains(Pattern pattern); + + /** + * Message as a byte array. + * + * @return The message as a byte array. + */ + @Override + ByteArray toByteArray(); + + /** + * Message as a {@code String}. + * + * @return The message as a {@code String}. + */ + @Override + String toString(); + + /** + * Create a copy of the {@code HttpRequest} in temporary file.
+ * This method is used to save the {@code HttpRequest} object to a temporary file, + * so that it is no longer held in memory. Extensions can use this method to convert + * {@code HttpRequest} objects into a form suitable for long-term usage. + * + * @return A new {@code HttpRequest} instance stored in temporary file. + */ + HttpRequest copyToTempFile(); + + /** + * Create a copy of the {@code HttpRequest} with the new service. + * + * @param service An {@link HttpService} reference to add. + * + * @return A new {@code HttpRequest} instance. + */ + @Override + HttpRequest withService(HttpService service); + + /** + * Create a copy of the {@code HttpRequest} with the new path. + * + * @param path The path to use. + * + * @return A new {@code HttpRequest} instance with updated path. + */ + @Override + HttpRequest withPath(String path); + + /** + * Create a copy of the {@code HttpRequest} with the new method. + * + * @param method the method to use + * + * @return a new {@code HttpRequest} instance with updated method. + */ + @Override + HttpRequest withMethod(String method); + + /** + * Create a copy of the {@code HttpRequest} with the added or updated header.
+ * If the header exists in the request, it is updated.
+ * If the header doesn't exist in the request, it is added. + * + * @param header HTTP header to add or update. + * + * @return A new {@code HttpRequest} with the added or updated header. + */ + @Override + HttpRequest withHeader(HttpHeader header); + + /** + * Create a copy of the {@code HttpRequest} with the added or updated header.
+ * If the header exists in the request, it is updated.
+ * If the header doesn't exist in the request, it is added. + * + * @param name The name of the header. + * @param value The value of the header. + * + * @return A new {@code HttpRequest} with the added or updated header. + */ + @Override + HttpRequest withHeader(String name, String value); + + /** + * Create a copy of the {@code HttpRequest} with the HTTP parameter.
+ * If the parameter exists in the request, it is updated.
+ * If the parameter doesn't exist in the request, it is added. + * + * @param parameters HTTP parameter to add or update. + * + * @return A new {@code HttpRequest} with the added or updated parameter. + */ + @Override + HttpRequest withParameter(HttpParameter parameters); + + /** + * Create a copy of the {@code HttpRequest} with the added HTTP parameters. + * + * @param parameters HTTP parameters to add. + * + * @return A new {@code HttpRequest} instance. + */ + @Override + HttpRequest withAddedParameters(List parameters); + + /** + * Create a copy of the {@code HttpRequest} with the added HTTP parameters. + * + * @param parameters HTTP parameters to add. + * + * @return A new {@code HttpRequest} instance. + */ + @Override + HttpRequest withAddedParameters(HttpParameter... parameters); + + /** + * Create a copy of the {@code HttpRequest} with the removed HTTP parameters. + * + * @param parameters HTTP parameters to remove. + * + * @return A new {@code HttpRequest} instance. + */ + @Override + HttpRequest withRemovedParameters(List parameters); + + /** + * Create a copy of the {@code HttpRequest} with the removed HTTP parameters. + * + * @param parameters HTTP parameters to remove. + * + * @return A new {@code HttpRequest} instance. + */ + @Override + HttpRequest withRemovedParameters(HttpParameter... parameters); + + /** + * Create a copy of the {@code HttpRequest} with the updated HTTP parameters.
+ * + * @param parameters HTTP parameters to update. + * + * @return A new {@code HttpRequest} instance. + */ + @Override + HttpRequest withUpdatedParameters(List parameters); + + /** + * Create a copy of the {@code HttpRequest} with the updated HTTP parameters.
+ * + * @param parameters HTTP parameters to update. + * + * @return A new {@code HttpRequest} instance. + */ + @Override + HttpRequest withUpdatedParameters(HttpParameter... parameters); + + /** + * Create a copy of the {@code HttpRequest} with the transformation applied. + * + * @param transformation Transformation to apply. + * + * @return A new {@code HttpRequest} instance. + */ + @Override + HttpRequest withTransformationApplied(HttpTransformation transformation); + + /** + * Create a copy of the {@code HttpRequest} with the updated body.
+ * Updates Content-Length header. + * + * @param body the new body for the request + * + * @return A new {@code HttpRequest} instance. + */ + @Override + HttpRequest withBody(String body); + + /** + * Create a copy of the {@code HttpRequest} with the updated body.
+ * Updates Content-Length header. + * + * @param body the new body for the request + * + * @return A new {@code HttpRequest} instance. + */ + @Override + HttpRequest withBody(ByteArray body); + + /** + * Create a copy of the {@code HttpRequest} with the added header. + * + * @param name The name of the header. + * @param value The value of the header. + * + * @return The updated HTTP request with the added header. + */ + @Override + HttpRequest withAddedHeader(String name, String value); + + /** + * Create a copy of the {@code HttpRequest} with the added header. + * + * @param header The {@link HttpHeader} to add to the HTTP request. + * + * @return The updated HTTP request with the added header. + */ + @Override + HttpRequest withAddedHeader(HttpHeader header); + + /** + * Create a copy of the {@code HttpRequest} with the updated header. + * + * @param name The name of the header to update the value of. + * @param value The new value of the specified HTTP header. + * + * @return The updated request containing the updated header. + */ + @Override + HttpRequest withUpdatedHeader(String name, String value); + + /** + * Create a copy of the {@code HttpRequest} with the updated header. + * + * @param header The {@link HttpHeader} to update containing the new value. + * + * @return The updated request containing the updated header. + */ + @Override + HttpRequest withUpdatedHeader(HttpHeader header); + + /** + * Removes an existing HTTP header from the current request. + * + * @param name The name of the HTTP header to remove from the request. + * + * @return The updated request containing the removed header. + */ + @Override + HttpRequest withRemovedHeader(String name); + + /** + * Removes an existing HTTP header from the current request. + * + * @param header The {@link HttpHeader} to remove from the request. + * + * @return The updated request containing the removed header. + */ + @Override + HttpRequest withRemovedHeader(HttpHeader header); + + /** + * Create a copy of the {@code HttpRequest} with the added markers. + * + * @param markers Request markers to add. + * + * @return A new {@link HttpRequest} instance. + */ + @Override + HttpRequest withMarkers(List markers); + + /** + * Create a copy of the {@code HttpRequest} with the added markers. + * + * @param markers Request markers to add. + * + * @return A new {@link HttpRequest} instance. + */ + @Override + HttpRequest withMarkers(Marker... markers); + + /** + * Create a copy of the {@code HttpRequest} with added default headers. + * + * @return a new {@link HttpRequest} with added default headers + */ + @Override + HttpRequest withDefaultHeaders(); + + /** + * This method retrieves a unique ID for this request/response. + * + * @return An identifier that is unique to a single request/response pair. + * Extensions can use this to correlate details of requests and responses + * and perform processing on the response message accordingly. + */ + @Override + int messageId(); + + /** + * This method retrieves the name of the Burp Proxy listener that is + * processing the intercepted message. + * + * @return The name of the Burp Proxy listener that is processing the + * intercepted message. The format is the same as that shown in the Proxy + * Listeners UI - for example, "127.0.0.1:8080". + */ + @Override + String listenerInterface(); + + /** + * This method retrieves the IP address for the source of the intercepted + * message. + * + * @return The IP address for the source of the intercepted message. + */ + @Override + InetAddress sourceIpAddress(); + + /** + * This method retrieves the IP address for the destination of the + * intercepted message. + * + * @return The IP address for the destination of the intercepted message. + */ + @Override + InetAddress destinationIpAddress(); +} diff --git a/src/burp/api/montoya/proxy/http/InterceptedResponse.java b/src/burp/api/montoya/proxy/http/InterceptedResponse.java new file mode 100644 index 0000000..d1d80d2 --- /dev/null +++ b/src/burp/api/montoya/proxy/http/InterceptedResponse.java @@ -0,0 +1,459 @@ +/* + * Copyright (c) 2022-2023. PortSwigger Ltd. All rights reserved. + * + * This code may be used to extend the functionality of Burp Suite Community Edition + * and Burp Suite Professional, provided that this usage does not violate the + * license terms for those products. + */ + +package burp.api.montoya.proxy.http; + +import burp.api.montoya.core.Annotations; +import burp.api.montoya.core.ByteArray; +import burp.api.montoya.core.Marker; +import burp.api.montoya.http.message.Cookie; +import burp.api.montoya.http.message.HttpHeader; +import burp.api.montoya.http.message.MimeType; +import burp.api.montoya.http.message.StatusCodeClass; +import burp.api.montoya.http.message.requests.HttpRequest; +import burp.api.montoya.http.message.responses.HttpResponse; +import burp.api.montoya.http.message.responses.analysis.Attribute; +import burp.api.montoya.http.message.responses.analysis.AttributeType; +import burp.api.montoya.http.message.responses.analysis.KeywordCount; + +import java.net.InetAddress; +import java.util.List; +import java.util.regex.Pattern; + +/** + * HTTP response intercepted by Burp Proxy. + */ +public interface InterceptedResponse extends InterceptedHttpMessage, HttpResponse +{ + /** + * @return initiatingRequest The HTTP request that was sent. + * @see InterceptedResponse#initiatingRequest() + */ + HttpRequest request(); + + /** + * @return initiatingRequest The HTTP request that was sent. + */ + HttpRequest initiatingRequest(); + + /** + * @return Annotations for request/response. + */ + Annotations annotations(); + + /** + * Obtain the HTTP status code contained in the response. + * + * @return HTTP status code. + */ + @Override + short statusCode(); + + /** + * Obtain the HTTP reason phrase contained in the response for HTTP 1 messages. + * HTTP 2 messages will return a mapped phrase based on the status code. + * + * @return HTTP Reason phrase. + */ + @Override + String reasonPhrase(); + + /** + * Test whether the status code is in the specified class. + * + * @param statusCodeClass The class of status code to test. + * + * @return True if the status code is in the class. + */ + @Override + boolean isStatusCodeClass(StatusCodeClass statusCodeClass); + + /** + * Return the HTTP Version text parsed from the response line for HTTP 1 messages. + * HTTP 2 messages will return "HTTP/2" + * + * @return Version string + */ + @Override + String httpVersion(); + + /** + * HTTP headers contained in the message. + * + * @return A list of HTTP headers. + */ + @Override + List headers(); + + /** + * Offset within the message where the message body begins. + * + * @return The message body offset. + */ + @Override + boolean hasHeader(HttpHeader header); + + /** + * @param name The name of the header to query within the request. + * + * @return True if a header exists in the request with the supplied name. + */ + @Override + boolean hasHeader(String name); + + /** + * @param name The name of the header to check. + * @param value The value of the header to check. + * + * @return True if a header exists in the request that matches the name and value supplied. + */ + @Override + boolean hasHeader(String name, String value); + + /** + * @param name The name of the header to retrieve. + * + * @return An instance of {@link HttpHeader} that matches the name supplied, {@code null} if no match found. + */ + @Override + HttpHeader header(String name); + + /** + * @param name The name of the header to retrieve. + * + * @return The {@code String} value of the header that matches the name supplied, {@code null} if no match found. + */ + @Override + String headerValue(String name); + + /** + * Body of a message as a byte array. + * + * @return The body of a message as a byte array. + */ + @Override + ByteArray body(); + + /** + * Body of a message as a {@code String}. + * + * @return The body of a message as a {@code String}. + */ + @Override + String bodyToString(); + + /** + * Offset within the message where the message body begins. + * + * @return The message body offset. + */ + @Override + int bodyOffset(); + + /** + * Markers for the message. + * + * @return A list of markers. + */ + @Override + List markers(); + + /** + * Obtain details of the HTTP cookies set in the response. + * + * @return A list of {@link Cookie} objects representing the cookies set in the response, if any. + */ + @Override + List cookies(); + + /** + * @param name The name of the cookie to find. + * + * @return An instance of {@link Cookie} that matches the name provided. {@code null} if not found. + */ + @Override + Cookie cookie(String name); + + /** + * @param name The name of the cookie to retrieve the value from. + * + * @return The value of the cookie that matches the name provided. {@code null} if not found. + */ + @Override + String cookieValue(String name); + + /** + * @param name The name of the cookie to check if it exists in the response. + * + * @return {@code true} If a cookie exists within the response that matches the name provided. {@code false} if not. + */ + @Override + boolean hasCookie(String name); + + /** + * @param cookie An instance of {@link Cookie} to check if it exists in the response. + * + * @return {@code true} If a cookie exists within the response that matches the {@link Cookie} provided. {@code false} if not. + */ + @Override + boolean hasCookie(Cookie cookie); + + /** + * Obtain the MIME type of the response, as determined by Burp Suite. + * + * @return The MIME type. + */ + @Override + MimeType mimeType(); + + /** + * Obtain the MIME type of the response, as stated in the HTTP headers. + * + * @return The stated MIME type. + */ + @Override + MimeType statedMimeType(); + + /** + * Obtain the MIME type of the response, as inferred from the contents of the HTTP message body. + * + * @return The inferred MIME type. + */ + @Override + MimeType inferredMimeType(); + + /** + * Retrieve the number of types given keywords appear in the response. + * + * @param keywords Keywords to count. + * + * @return List of keyword counts in the order they were provided. + */ + @Override + List keywordCounts(String... keywords); + + /** + * Retrieve the values of response attributes. + * + * @param types Response attributes to retrieve values for. + * + * @return List of {@link Attribute} objects. + */ + @Override + List attributes(AttributeType... types); + + /** + * Searches the data in the HTTP message for the specified search term. + * + * @param searchTerm The value to be searched for. + * @param caseSensitive Flags whether the search is case-sensitive. + * + * @return True if the search term is found. + */ + @Override + boolean contains(String searchTerm, boolean caseSensitive); + + /** + * Searches the data in the HTTP message for the specified regular expression. + * + * @param pattern The regular expression to be searched for. + * + * @return True if the pattern is matched. + */ + @Override + boolean contains(Pattern pattern); + + /** + * Message as a byte array. + * + * @return The message as a byte array. + */ + @Override + ByteArray toByteArray(); + + /** + * Message as a {@code String}. + * + * @return The message as a {@code String}. + */ + @Override + String toString(); + + /** + * Create a copy of the {@code HttpResponse} with the provided status code. + * + * @param statusCode the new status code for response + * + * @return A new {@code HttpResponse} instance. + */ + @Override + HttpResponse withStatusCode(short statusCode); + + /** + * Create a copy of the {@code HttpResponse} with the new reason phrase. + * + * @param reasonPhrase the new reason phrase for response + * + * @return A new {@code HttpResponse} instance. + */ + @Override + HttpResponse withReasonPhrase(String reasonPhrase); + + /** + * Create a copy of the {@code HttpResponse} with the new http version. + * + * @param httpVersion the new http version for response + * + * @return A new {@code HttpResponse} instance. + */ + @Override + HttpResponse withHttpVersion(String httpVersion); + + /** + * Create a copy of the {@code HttpResponse} with the updated body.
+ * Updates Content-Length header. + * + * @param body the new body for the response + * + * @return A new {@code HttpResponse} instance. + */ + @Override + HttpResponse withBody(String body); + + /** + * Create a copy of the {@code HttpResponse} with the updated body.
+ * Updates Content-Length header. + * + * @param body the new body for the response + * + * @return A new {@code HttpResponse} instance. + */ + @Override + HttpResponse withBody(ByteArray body); + + /** + * Create a copy of the {@code HttpResponse} with the added header. + * + * @param header The {@link HttpHeader} to add to the response. + * + * @return The updated response containing the added header. + */ + @Override + HttpResponse withAddedHeader(HttpHeader header); + + /** + * Create a copy of the {@code HttpResponse} with the added header. + * + * @param name The name of the header. + * @param value The value of the header. + * + * @return The updated response containing the added header. + */ + @Override + HttpResponse withAddedHeader(String name, String value); + + /** + * Create a copy of the {@code HttpResponse} with the updated header. + * + * @param header The {@link HttpHeader} to update containing the new value. + * + * @return The updated response containing the updated header. + */ + @Override + HttpResponse withUpdatedHeader(HttpHeader header); + + /** + * Create a copy of the {@code HttpResponse} with the updated header. + * + * @param name The name of the header to update the value of. + * @param value The new value of the specified HTTP header. + * + * @return The updated response containing the updated header. + */ + @Override + HttpResponse withUpdatedHeader(String name, String value); + + /** + * Create a copy of the {@code HttpResponse} with the removed header. + * + * @param header The {@link HttpHeader} to remove from the response. + * + * @return The updated response containing the removed header. + */ + @Override + HttpResponse withRemovedHeader(HttpHeader header); + + /** + * Create a copy of the {@code HttpResponse} with the removed header. + * + * @param name The name of the HTTP header to remove from the response. + * + * @return The updated response containing the removed header. + */ + @Override + HttpResponse withRemovedHeader(String name); + + /** + * Create a copy of the {@code HttpResponse} with the added markers. + * + * @param markers Request markers to add. + * + * @return A new {@code MarkedHttpRequestResponse} instance. + */ + @Override + HttpResponse withMarkers(List markers); + + /** + * Create a copy of the {@code HttpResponse} with the added markers. + * + * @param markers Request markers to add. + * + * @return A new {@code MarkedHttpRequestResponse} instance. + */ + @Override + HttpResponse withMarkers(Marker... markers); + + /** + * This method retrieves a unique ID for this request/response. + * + * @return An identifier that is unique to a single request/response pair. + * Extensions can use this to correlate details of requests and responses + * and perform processing on the response message accordingly. + */ + @Override + int messageId(); + + /** + * This method retrieves the name of the Burp Proxy listener that is + * processing the intercepted message. + * + * @return The name of the Burp Proxy listener that is processing the + * intercepted message. The format is the same as that shown in the Proxy + * Listeners UI - for example, "127.0.0.1:8080". + */ + @Override + String listenerInterface(); + + /** + * This method retrieves the IP address for the source of the intercepted + * message. + * + * @return The IP address for the source of the intercepted message. + */ + @Override + InetAddress sourceIpAddress(); + + /** + * This method retrieves the IP address for the destination of the + * intercepted message. + * + * @return The IP address for the destination of the intercepted message. + */ + @Override + InetAddress destinationIpAddress(); +} diff --git a/src/burp/api/montoya/proxy/http/ProxyRequestHandler.java b/src/burp/api/montoya/proxy/http/ProxyRequestHandler.java new file mode 100644 index 0000000..84b4eb6 --- /dev/null +++ b/src/burp/api/montoya/proxy/http/ProxyRequestHandler.java @@ -0,0 +1,46 @@ +/* + * Copyright (c) 2022-2023. PortSwigger Ltd. All rights reserved. + * + * This code may be used to extend the functionality of Burp Suite Community Edition + * and Burp Suite Professional, provided that this usage does not violate the + * license terms for those products. + */ + +package burp.api.montoya.proxy.http; + +import burp.api.montoya.proxy.Proxy; + +/** + * Extensions can implement this interface and then call + * {@link Proxy#registerRequestHandler(ProxyRequestHandler)} to register a + * Proxy request handler. The handler will be notified of requests being + * processed by the Proxy tool. Extensions can perform custom analysis or + * modification of these messages, and control in-UI message interception. + */ +public interface ProxyRequestHandler +{ + /** + * This method is invoked before an HTTP request is received by the Proxy.
+ * Can modify the request.
+ * Can modify the annotations.
+ * Can control whether the request should be intercepted and displayed to the user for manual review or modification.
+ * Can drop the request.
+ * + * @param interceptedRequest An {@link InterceptedRequest} object that extensions can use to query and update details of the request. + * + * @return The {@link ProxyRequestReceivedAction} containing the required action, annotations and HTTP request to be passed through the proxy. + */ + ProxyRequestReceivedAction handleRequestReceived(InterceptedRequest interceptedRequest); + + /** + * This method is invoked after an HTTP request has been processed by the Proxy before it is sent.
+ * Can modify the request.
+ * Can modify the annotations.
+ * Can control whether the request is sent or dropped.
+ * + * @param interceptedRequest An {@link InterceptedRequest} object that extensions can use to query and update details of the intercepted request. + * + * @return The {@link ProxyRequestToBeSentAction} containing the required action, annotations and HTTP request to be sent from the proxy. + */ + ProxyRequestToBeSentAction handleRequestToBeSent(InterceptedRequest interceptedRequest); +} diff --git a/src/burp/api/montoya/proxy/http/ProxyRequestReceivedAction.java b/src/burp/api/montoya/proxy/http/ProxyRequestReceivedAction.java new file mode 100644 index 0000000..66f4f8c --- /dev/null +++ b/src/burp/api/montoya/proxy/http/ProxyRequestReceivedAction.java @@ -0,0 +1,176 @@ +/* + * Copyright (c) 2022-2023. PortSwigger Ltd. All rights reserved. + * + * This code may be used to extend the functionality of Burp Suite Community Edition + * and Burp Suite Professional, provided that this usage does not violate the + * license terms for those products. + */ + +package burp.api.montoya.proxy.http; + +import burp.api.montoya.core.Annotations; +import burp.api.montoya.http.message.requests.HttpRequest; +import burp.api.montoya.proxy.MessageReceivedAction; + +import static burp.api.montoya.internal.ObjectFactoryLocator.FACTORY; + +/** + * Extensions can implement this interface when returning a result from + * {@link ProxyRequestHandler#handleRequestReceived(InterceptedRequest)}. + */ +public interface ProxyRequestReceivedAction +{ + /** + * This method retrieves the current initial intercept action. + * + * @return The {@link MessageReceivedAction}. + */ + MessageReceivedAction action(); + + /** + * This method retrieves the current HTTP request to forward after any + * modifications by the extension. + * + * @return The {@link HttpRequest} to forward after any modifications by + * the extension. + */ + HttpRequest request(); + + /** + * This method retrieves the annotations for the current request after any + * modifications by the extension. + * + * @return The {@link Annotations} for the intercepted HTTP request. + */ + Annotations annotations(); + + /** + * This method can be used to create a result that causes Burp Proxy to + * follow the current interception rules to determine the appropriate + * action to take for the request.
+ * Annotations are not modified. + * + * @param request The {@link HttpRequest} received after any modifications + * by the extension. + * + * @return The {@link ProxyRequestReceivedAction} that allows user rules to be + * followed. + */ + static ProxyRequestReceivedAction continueWith(HttpRequest request) + { + return FACTORY.requestInitialInterceptResultFollowUserRules(request); + } + + /** + * This method can be used to create a result that causes Burp Proxy to + * follow the current interception rules to determine the appropriate + * action to take for the request. + * + * @param request The {@link HttpRequest} received after any modifications + * by the extension. + * @param annotations The {@link Annotations} for the intercepted + * HTTP request. + * + * @return The {@link ProxyRequestReceivedAction} that causes Burp Proxy + * to follow the current interception rules to determine the appropriate + * action to take for the request. + */ + static ProxyRequestReceivedAction continueWith(HttpRequest request, Annotations annotations) + { + return FACTORY.requestInitialInterceptResultFollowUserRules(request, annotations); + } + + /** + * This method can be used to create a result that causes Burp Proxy to + * present the request to the user for manual review or modification.
+ * Annotations are not modified. + * + * @param request The {@link HttpRequest} received after any modifications + * by the extension. + * + * @return The {@link ProxyRequestReceivedAction} that causes Burp Proxy + * to present the request to the user for manual review or modification. + */ + static ProxyRequestReceivedAction intercept(HttpRequest request) + { + return FACTORY.requestInitialInterceptResultIntercept(request); + } + + /** + * This method can be used to create a result that causes Burp Proxy to + * present the request to the user for manual review or modification. + * + * @param request The {@link HttpRequest} received after any modifications + * by the extension. + * @param annotations The {@link Annotations} for the intercepted + * HTTP request. + * + * @return The {@link ProxyRequestReceivedAction} that causes Burp Proxy + * to present the request to the user for manual review or modification. + */ + static ProxyRequestReceivedAction intercept(HttpRequest request, Annotations annotations) + { + return FACTORY.requestInitialInterceptResultIntercept(request, annotations); + } + + /** + * This method can be used to create a result that causes Burp Proxy to + * forward the request without presenting it to the user.
+ * Annotations are not modified. + * + * @param request The {@link HttpRequest} received after any modifications + * by the extension. + * + * @return The {@link ProxyRequestReceivedAction} that causes Burp Proxy + * to forward the request without presenting it to the user. + */ + static ProxyRequestReceivedAction doNotIntercept(HttpRequest request) + { + return FACTORY.requestInitialInterceptResultDoNotIntercept(request); + } + + /** + * This method can be used to create a result that causes Burp Proxy to + * forward the request without presenting it to the user. + * + * @param request The {@link HttpRequest} received after any modifications + * by the extension. + * @param annotations The {@link Annotations} for the intercepted + * HTTP request. + * + * @return The {@link ProxyRequestReceivedAction} that causes Burp Proxy + * to forward the request without presenting it to the user. + */ + static ProxyRequestReceivedAction doNotIntercept(HttpRequest request, Annotations annotations) + { + return FACTORY.requestInitialInterceptResultDoNotIntercept(request, annotations); + } + + /** + * This method can be used to create a result that causes Burp Proxy to + * drop the request. + * + * @return The {@link ProxyRequestReceivedAction} that causes Burp Proxy + * to drop the request. + */ + static ProxyRequestReceivedAction drop() + { + return FACTORY.requestInitialInterceptResultDrop(); + } + + /** + * This method can be used to create a default implementation of an initial + * intercept result for an HTTP request. + * + * @param request The {@link HttpRequest} received after any modifications by the extension. + * @param annotations The {@link Annotations} for the intercepted HTTP request. {@code null} value will leave the annotations unmodified. + * @param action The {@link MessageReceivedAction} for the HTTP request. + * + * @return The {@link ProxyRequestReceivedAction} including the HTTP + * request, annotations and initial intercept action. + */ + static ProxyRequestReceivedAction proxyRequestReceivedAction(HttpRequest request, Annotations annotations, MessageReceivedAction action) + { + return FACTORY.proxyRequestReceivedAction(request, annotations, action); + } +} diff --git a/src/burp/api/montoya/proxy/http/ProxyRequestToBeSentAction.java b/src/burp/api/montoya/proxy/http/ProxyRequestToBeSentAction.java new file mode 100644 index 0000000..682e272 --- /dev/null +++ b/src/burp/api/montoya/proxy/http/ProxyRequestToBeSentAction.java @@ -0,0 +1,107 @@ +/* + * Copyright (c) 2022-2023. PortSwigger Ltd. All rights reserved. + * + * This code may be used to extend the functionality of Burp Suite Community Edition + * and Burp Suite Professional, provided that this usage does not violate the + * license terms for those products. + */ + +package burp.api.montoya.proxy.http; + +import burp.api.montoya.core.Annotations; +import burp.api.montoya.http.message.requests.HttpRequest; +import burp.api.montoya.proxy.MessageToBeSentAction; + +import static burp.api.montoya.internal.ObjectFactoryLocator.FACTORY; + +/** + * Extensions can implement this interface when returning a result from + * {@link ProxyRequestHandler#handleRequestToBeSent(InterceptedRequest)}. + */ +public interface ProxyRequestToBeSentAction +{ + /** + * This method retrieves the current final intercept action. + * + * @return The {@link MessageToBeSentAction}. + */ + MessageToBeSentAction action(); + + /** + * This method retrieves the current HTTP request to forward after any + * modifications by the extension. + * + * @return The {@link HttpRequest} to forward after any modifications by + * the extension. + */ + HttpRequest request(); + + /** + * This method retrieves the annotations for the current request after any + * modifications by the extension. + * + * @return The {@link Annotations} for the intercepted HTTP request. + */ + Annotations annotations(); + + /** + * This method can be used to create a result that causes Burp Proxy to + * forward the request.
+ * Annotations are not modified. + * + * @param request The {@link HttpRequest} to forward after any + * modifications by the extension. + * + * @return The {@link ProxyRequestToBeSentAction} that causes Burp Proxy + * to forward the request. + */ + static ProxyRequestToBeSentAction continueWith(HttpRequest request) + { + return FACTORY.requestFinalInterceptResultContinueWith(request); + } + + /** + * This method can be used to create a result that causes Burp Proxy to + * forward the request. + * + * @param request The {@link HttpRequest} to forward after any + * modifications by the extension. + * @param annotations The {@link Annotations} for the intercepted + * HTTP request. + * + * @return The {@link ProxyRequestToBeSentAction} that causes Burp Proxy + * to forward the request. + */ + static ProxyRequestToBeSentAction continueWith(HttpRequest request, Annotations annotations) + { + return FACTORY.requestFinalInterceptResultContinueWith(request, annotations); + } + + /** + * This method can be used to create a result that causes Burp Proxy to + * drop the request. + * + * @return The {@link ProxyRequestToBeSentAction} that causes Burp Proxy + * to drop the request. + */ + static ProxyRequestToBeSentAction drop() + { + return FACTORY.requestFinalInterceptResultDrop(); + } + + /** + * This method can be used to create a default implementation of a final + * intercept result for an HTTP request. + * + * @param request The {@link HttpRequest} to forward after any modifications by the extension. + * @param annotations The {@link Annotations} for the intercepted HTTP request. {@code null} value will leave the annotations unmodified. + * @param action The {@link MessageToBeSentAction} for the HTTP request. + * + * @return The {@link ProxyRequestToBeSentAction} including the HTTP + * request, annotations and final intercept action. + */ + static ProxyRequestToBeSentAction proxyRequestToBeSentAction(HttpRequest request, Annotations annotations, MessageToBeSentAction action) + { + return FACTORY.proxyRequestToBeSentAction(request, annotations, action); + } +} diff --git a/src/burp/api/montoya/proxy/http/ProxyResponseHandler.java b/src/burp/api/montoya/proxy/http/ProxyResponseHandler.java new file mode 100644 index 0000000..be26d9d --- /dev/null +++ b/src/burp/api/montoya/proxy/http/ProxyResponseHandler.java @@ -0,0 +1,44 @@ +/* + * Copyright (c) 2022-2023. PortSwigger Ltd. All rights reserved. + * + * This code may be used to extend the functionality of Burp Suite Community Edition + * and Burp Suite Professional, provided that this usage does not violate the + * license terms for those products. + */ + +package burp.api.montoya.proxy.http; + +import burp.api.montoya.proxy.Proxy; + +/** + * Extensions can implement this interface and then call + * {@link Proxy#registerResponseHandler(ProxyResponseHandler)} to register a + * Proxy response handler. The handler will be notified of responses being + * processed by the Proxy tool. Extensions can perform custom analysis or + * modification of these responses, and control in-UI message interception. + */ +public interface ProxyResponseHandler +{ + /** + * This method is invoked when an HTTP response is received in the Proxy. + * + * @param interceptedResponse An {@link InterceptedResponse} object + * that extensions can use to query and update details of the response, and + * control whether the response should be intercepted and displayed to the + * user for manual review or modification. + * + * @return The {@link ProxyResponseReceivedAction} containing the required action, HTTP response and annotations to be passed through. + */ + ProxyResponseReceivedAction handleResponseReceived(InterceptedResponse interceptedResponse); + + /** + * This method is invoked when an HTTP response has been processed by the + * Proxy before it is returned to the client. + * + * @param interceptedResponse An {@link InterceptedResponse} object + * that extensions can use to query and update details of the response. + * + * @return The {@link ProxyResponseToBeSentAction} containing the required action, HTTP response and annotations to be passed through. + */ + ProxyResponseToBeSentAction handleResponseToBeSent(InterceptedResponse interceptedResponse); +} diff --git a/src/burp/api/montoya/proxy/http/ProxyResponseReceivedAction.java b/src/burp/api/montoya/proxy/http/ProxyResponseReceivedAction.java new file mode 100644 index 0000000..e0c9bac --- /dev/null +++ b/src/burp/api/montoya/proxy/http/ProxyResponseReceivedAction.java @@ -0,0 +1,178 @@ +/* + * Copyright (c) 2022-2023. PortSwigger Ltd. All rights reserved. + * + * This code may be used to extend the functionality of Burp Suite Community Edition + * and Burp Suite Professional, provided that this usage does not violate the + * license terms for those products. + */ + +package burp.api.montoya.proxy.http; + +import burp.api.montoya.core.Annotations; +import burp.api.montoya.http.message.responses.HttpResponse; +import burp.api.montoya.proxy.MessageReceivedAction; + +import static burp.api.montoya.internal.ObjectFactoryLocator.FACTORY; + +/** + * Extensions can implement this interface when returning a result from + * {@link ProxyResponseHandler#handleResponseReceived(InterceptedResponse)}. + */ +public interface ProxyResponseReceivedAction +{ + /** + * This method retrieves the current initial intercept action. + * + * @return The {@link MessageReceivedAction}. + */ + MessageReceivedAction action(); + + /** + * This method retrieves the current HTTP response to forward after any + * modifications by the extension. + * + * @return The {@link HttpResponse} to forward after any modifications by + * the extension. + */ + HttpResponse response(); + + /** + * This method retrieves the annotations for the current response after any + * modifications by the extension. + * + * @return The {@link Annotations} for the intercepted HTTP response. + */ + Annotations annotations(); + + /** + * This method can be used to create an action that causes Burp Proxy to + * follow the current interception rules to determine the appropriate + * action to take for the response.
+ * Annotations are not modified. + * + * @param response The {@link HttpResponse} received after any + * modifications by the extension. + * + * @return The {@link ProxyResponseReceivedAction} that causes Burp + * Proxy to follow the current interception rules to determine the + * appropriate action to take for the response. + */ + static ProxyResponseReceivedAction continueWith(HttpResponse response) + { + return FACTORY.responseInitialInterceptResultFollowUserRules(response); + } + + /** + * This method can be used to create an action that causes Burp Proxy to + * follow the current interception rules to determine the appropriate + * action to take for the response. + * + * @param response The {@link HttpResponse} received after any + * modifications by the extension. + * @param annotations The {@link Annotations} for the intercepted + * HTTP response. + * + * @return The {@link ProxyResponseReceivedAction} that causes Burp + * Proxy to follow the current interception rules to determine the + * appropriate action to take for the response. + */ + static ProxyResponseReceivedAction continueWith(HttpResponse response, Annotations annotations) + { + return FACTORY.responseInitialInterceptResultFollowUserRules(response, annotations); + } + + /** + * This method can be used to create an action that causes Burp Proxy to + * present the response to the user for manual review or modification.
+ * Annotations are not modified. + * + * @param response The {@link HttpResponse} received after any + * modifications by the extension. + * + * @return The {@link ProxyResponseReceivedAction} that causes Burp + * Proxy to present the response to the user for manual review or + * modification. + */ + static ProxyResponseReceivedAction intercept(HttpResponse response) + { + return FACTORY.responseInitialInterceptResultIntercept(response); + } + + /** + * This method can be used to create an action that causes Burp Proxy to + * present the response to the user for manual review or modification. + * + * @param response The {@link HttpResponse} received after any + * modifications by the extension. + * @param annotations The {@link Annotations} for the intercepted + * HTTP response. + * + * @return The {@link ProxyResponseReceivedAction} that causes Burp + * Proxy to present the response to the user for manual review or + * modification. + */ + static ProxyResponseReceivedAction intercept(HttpResponse response, Annotations annotations) + { + return FACTORY.responseInitialInterceptResultIntercept(response, annotations); + } + + /** + * This method can be used to create an action that causes Burp Proxy to + * forward the response without presenting it to the user.
+ * Annotations are not modified. + * + * @param response The {@link HttpResponse} received after any + * modifications by the extension. + * + * @return The {@link ProxyResponseReceivedAction} that causes Burp + * Proxy to forward the response without presenting it to the user. + */ + static ProxyResponseReceivedAction doNotIntercept(HttpResponse response) + { + return FACTORY.responseInitialInterceptResultDoNotIntercept(response); + } + + /** + * This method can be used to create an action that causes Burp Proxy to + * forward the response without presenting it to the user. + * + * @param response The {@link HttpResponse} received after any + * modifications by the extension. + * @param annotations The {@link Annotations} for the intercepted + * HTTP response. + * + * @return The {@link ProxyResponseReceivedAction} that causes Burp + * Proxy to forward the response without presenting it to the user. + */ + static ProxyResponseReceivedAction doNotIntercept(HttpResponse response, Annotations annotations) + { + return FACTORY.responseInitialInterceptResultDoNotIntercept(response, annotations); + } + + /** + * This method can be used to create an action that causes Burp Proxy to + * drop the response. + * + * @return The {@link ProxyResponseReceivedAction} that causes Burp + * Proxy to drop the response. + */ + static ProxyResponseReceivedAction drop() + { + return FACTORY.responseInitialInterceptResultDrop(); + } + + /** + * This method can be used to create a default implementation of a {@link ProxyResponseReceivedAction} + * for an HTTP response. + * + * @param response The {@link HttpResponse} received after any modifications by the extension. + * @param annotations The {@link Annotations} for the intercepted HTTP response. {@code null} value will leave the annotations unmodified. + * @param action The {@link MessageReceivedAction} for the HTTP response. + * + * @return The {@link ProxyResponseReceivedAction} including the HTTP response, annotations and intercept action. + */ + static ProxyResponseReceivedAction proxyResponseReceivedAction(HttpResponse response, Annotations annotations, MessageReceivedAction action) + { + return FACTORY.proxyResponseReceivedAction(response, annotations, action); + } +} diff --git a/src/burp/api/montoya/proxy/http/ProxyResponseToBeSentAction.java b/src/burp/api/montoya/proxy/http/ProxyResponseToBeSentAction.java new file mode 100644 index 0000000..593102b --- /dev/null +++ b/src/burp/api/montoya/proxy/http/ProxyResponseToBeSentAction.java @@ -0,0 +1,105 @@ +/* + * Copyright (c) 2022-2023. PortSwigger Ltd. All rights reserved. + * + * This code may be used to extend the functionality of Burp Suite Community Edition + * and Burp Suite Professional, provided that this usage does not violate the + * license terms for those products. + */ + +package burp.api.montoya.proxy.http; + +import burp.api.montoya.core.Annotations; +import burp.api.montoya.http.message.responses.HttpResponse; +import burp.api.montoya.proxy.MessageToBeSentAction; + +import static burp.api.montoya.internal.ObjectFactoryLocator.FACTORY; + +/** + * Extensions can implement this interface when returning a result from + * {@link ProxyResponseHandler#handleResponseToBeSent(InterceptedResponse)}. + */ +public interface ProxyResponseToBeSentAction +{ + /** + * This method retrieves the current final intercept action. + * + * @return The {@link MessageToBeSentAction}. + */ + MessageToBeSentAction action(); + + /** + * This method retrieves the current HTTP response to forward after any + * modifications by the extension. + * + * @return The {@link HttpResponse} to forward after any modifications by + * the extension. + */ + HttpResponse response(); + + /** + * This method retrieves the annotations for the current response after any + * modifications by the extension. + * + * @return The {@link Annotations} for the intercepted HTTP response. + */ + Annotations annotations(); + + /** + * This method can be used to create a result that causes Burp Proxy to + * forward the response.
+ * Annotations are not modified. + * + * @param response The {@link HttpResponse} to forward after any + * modifications by the extension. + * + * @return The {@link ProxyResponseToBeSentAction} that causes Burp Proxy + * to forward the response. + */ + static ProxyResponseToBeSentAction continueWith(HttpResponse response) + { + return FACTORY.responseFinalInterceptResultContinueWith(response); + } + + /** + * This method can be used to create a result that causes Burp Proxy to + * forward the response. + * + * @param response The {@link HttpResponse} to forward after any modifications by the extension. + * @param annotations The {@link Annotations} for the intercepted HTTP response. + * + * @return The {@link ProxyResponseToBeSentAction} that causes Burp Proxy + * to forward the response. + */ + static ProxyResponseToBeSentAction continueWith(HttpResponse response, Annotations annotations) + { + return FACTORY.responseFinalInterceptResultContinueWith(response, annotations); + } + + /** + * This method can be used to create a result that causes Burp Proxy to + * drop the response. + * + * @return The {@link ProxyResponseToBeSentAction} that causes Burp Proxy + * to drop the response. + */ + static ProxyResponseToBeSentAction drop() + { + return FACTORY.responseFinalInterceptResultDrop(); + } + + /** + * This method can be used to create a default implementation of a final + * intercept result for an HTTP response. + * + * @param response The {@link HttpResponse} to forward after any modifications by the extension. + * @param annotations The {@link Annotations} for the intercepted HTTP response. {@code null} value will leave the annotations unmodified. + * @param action The {@link MessageToBeSentAction} for the HTTP response. + * + * @return The {@link ProxyResponseToBeSentAction} including the HTTP + * response, annotations and final intercept action. + */ + static ProxyResponseToBeSentAction proxyResponseToReturnAction(HttpResponse response, Annotations annotations, MessageToBeSentAction action) + { + return FACTORY.proxyResponseToReturnAction(response, annotations, action); + } +} diff --git a/src/burp/api/montoya/proxy/websocket/BinaryMessageReceivedAction.java b/src/burp/api/montoya/proxy/websocket/BinaryMessageReceivedAction.java new file mode 100644 index 0000000..0b49cf1 --- /dev/null +++ b/src/burp/api/montoya/proxy/websocket/BinaryMessageReceivedAction.java @@ -0,0 +1,120 @@ +/* + * Copyright (c) 2022-2023. PortSwigger Ltd. All rights reserved. + * + * This code may be used to extend the functionality of Burp Suite Community Edition + * and Burp Suite Professional, provided that this usage does not violate the + * license terms for those products. + */ + +package burp.api.montoya.proxy.websocket; + +import burp.api.montoya.core.ByteArray; +import burp.api.montoya.proxy.MessageReceivedAction; +import burp.api.montoya.websocket.BinaryMessage; + +import static burp.api.montoya.internal.ObjectFactoryLocator.FACTORY; + +/** + * Extensions can implement this interface when returning a binary message from + * {@link ProxyMessageHandler#handleBinaryMessageReceived(InterceptedBinaryMessage)}. + */ +public interface BinaryMessageReceivedAction +{ + /** + * @return The action associated with this message. + */ + MessageReceivedAction action(); + + /** + * @return The payload of this message. + */ + ByteArray payload(); + + /** + * Build a binary WebSocket message to + * follow the current interception rules to determine the appropriate + * action to take for the message. + * + * @param payload The binary message payload. + * + * @return The {@link BinaryMessageReceivedAction} that allows user rules to be + * followed. + */ + static BinaryMessageReceivedAction continueWith(ByteArray payload) + { + return FACTORY.followUserRulesInitialProxyBinaryMessage(payload); + } + + /** + * Build a binary WebSocket message to + * follow the current interception rules to determine the appropriate + * action to take for the message. + * + * @param message The binary message. + * + * @return The {@link BinaryMessageReceivedAction} that allows user rules to be + * followed. + */ + static BinaryMessageReceivedAction continueWith(BinaryMessage message) + { + return FACTORY.followUserRulesInitialProxyBinaryMessage(message.payload()); + } + + /** + * Build a binary WebSocket message to be intercepted within the Proxy. + * + * @param payload The binary message payload. + * + * @return The message. + */ + static BinaryMessageReceivedAction intercept(ByteArray payload) + { + return FACTORY.interceptInitialProxyBinaryMessage(payload); + } + + /** + * Build a binary WebSocket message to be intercepted within the Proxy. + * + * @param message The binary message. + * + * @return The message. + */ + static BinaryMessageReceivedAction intercept(BinaryMessage message) + { + return FACTORY.interceptInitialProxyBinaryMessage(message.payload()); + } + + /** + * Build a binary WebSocket message to continue within the Proxy without interception. + * + * @param payload The binary message payload. + * + * @return The message. + */ + static BinaryMessageReceivedAction doNotIntercept(ByteArray payload) + { + return FACTORY.doNotInterceptInitialProxyBinaryMessage(payload); + } + + /** + * Build a binary WebSocket message to continue within the Proxy without interception. + * + * @param message The binary message. + * + * @return The message. + */ + static BinaryMessageReceivedAction doNotIntercept(BinaryMessage message) + { + return FACTORY.doNotInterceptInitialProxyBinaryMessage(message.payload()); + } + + /** + * Build a binary WebSocket message to be dropped. + * + * @return The message to be dropped. + */ + static BinaryMessageReceivedAction drop() + { + return FACTORY.dropInitialProxyBinaryMessage(); + } +} diff --git a/src/burp/api/montoya/proxy/websocket/BinaryMessageToBeSentAction.java b/src/burp/api/montoya/proxy/websocket/BinaryMessageToBeSentAction.java new file mode 100644 index 0000000..386130a --- /dev/null +++ b/src/burp/api/montoya/proxy/websocket/BinaryMessageToBeSentAction.java @@ -0,0 +1,66 @@ +/* + * Copyright (c) 2022-2023. PortSwigger Ltd. All rights reserved. + * + * This code may be used to extend the functionality of Burp Suite Community Edition + * and Burp Suite Professional, provided that this usage does not violate the + * license terms for those products. + */ + +package burp.api.montoya.proxy.websocket; + +import burp.api.montoya.core.ByteArray; +import burp.api.montoya.proxy.MessageToBeSentAction; +import burp.api.montoya.websocket.BinaryMessage; + +import static burp.api.montoya.internal.ObjectFactoryLocator.FACTORY; + +/** + * Extensions can implement this interface when returning a binary message from + * {@link ProxyMessageHandler#handleBinaryMessageToBeSent(InterceptedBinaryMessage)}. + */ +public interface BinaryMessageToBeSentAction +{ + /** + * @return The action associated with this message. + */ + MessageToBeSentAction action(); + + /** + * @return The payload of this message. + */ + ByteArray payload(); + + /** + * Build a binary WebSocket message to continue through Burp. + * + * @param payload The binary message payload. + * + * @return The message. + */ + static BinaryMessageToBeSentAction continueWith(ByteArray payload) + { + return FACTORY.continueWithFinalProxyBinaryMessage(payload); + } + + /** + * Build a binary WebSocket message to continue through Burp. + * + * @param message The binary message. + * + * @return The message. + */ + static BinaryMessageToBeSentAction continueWith(BinaryMessage message) + { + return FACTORY.continueWithFinalProxyBinaryMessage(message.payload()); + } + + /** + * Build a binary WebSocket message to be dropped. + * + * @return The message to be dropped. + */ + static BinaryMessageToBeSentAction drop() + { + return FACTORY.dropFinalProxyBinaryMessage(); + } +} diff --git a/src/burp/api/montoya/proxy/websocket/InterceptedBinaryMessage.java b/src/burp/api/montoya/proxy/websocket/InterceptedBinaryMessage.java new file mode 100644 index 0000000..1b27e2c --- /dev/null +++ b/src/burp/api/montoya/proxy/websocket/InterceptedBinaryMessage.java @@ -0,0 +1,34 @@ +/* + * Copyright (c) 2022-2023. PortSwigger Ltd. All rights reserved. + * + * This code may be used to extend the functionality of Burp Suite Community Edition + * and Burp Suite Professional, provided that this usage does not violate the + * license terms for those products. + */ + +package burp.api.montoya.proxy.websocket; + +import burp.api.montoya.core.Annotations; +import burp.api.montoya.core.ByteArray; +import burp.api.montoya.websocket.BinaryMessage; +import burp.api.montoya.websocket.Direction; + +public interface InterceptedBinaryMessage extends BinaryMessage +{ + /** + * @return The annotations. + */ + Annotations annotations(); + + /** + * @return Binary based WebSocket payload. + */ + @Override + ByteArray payload(); + + /** + * @return The direction of the message. + */ + @Override + Direction direction(); +} diff --git a/src/burp/api/montoya/proxy/websocket/InterceptedTextMessage.java b/src/burp/api/montoya/proxy/websocket/InterceptedTextMessage.java new file mode 100644 index 0000000..ed8b0ef --- /dev/null +++ b/src/burp/api/montoya/proxy/websocket/InterceptedTextMessage.java @@ -0,0 +1,33 @@ +/* + * Copyright (c) 2022-2023. PortSwigger Ltd. All rights reserved. + * + * This code may be used to extend the functionality of Burp Suite Community Edition + * and Burp Suite Professional, provided that this usage does not violate the + * license terms for those products. + */ + +package burp.api.montoya.proxy.websocket; + +import burp.api.montoya.core.Annotations; +import burp.api.montoya.websocket.Direction; +import burp.api.montoya.websocket.TextMessage; + +public interface InterceptedTextMessage extends TextMessage +{ + /** + * @return The annotations. + */ + Annotations annotations(); + + /** + * @return Text based WebSocket payload. + */ + @Override + String payload(); + + /** + * @return The direction of the message. + */ + @Override + Direction direction(); +} diff --git a/src/burp/api/montoya/proxy/websocket/ProxyMessageHandler.java b/src/burp/api/montoya/proxy/websocket/ProxyMessageHandler.java new file mode 100644 index 0000000..2528115 --- /dev/null +++ b/src/burp/api/montoya/proxy/websocket/ProxyMessageHandler.java @@ -0,0 +1,66 @@ +/* + * Copyright (c) 2022-2023. PortSwigger Ltd. All rights reserved. + * + * This code may be used to extend the functionality of Burp Suite Community Edition + * and Burp Suite Professional, provided that this usage does not violate the + * license terms for those products. + */ + +package burp.api.montoya.proxy.websocket; + +/** + * This interface allows an extension to be notified when messages are sent or received via the proxy WebSocket, or it has been closed. + */ +public interface ProxyMessageHandler +{ + /** + * Invoked when a text message is received from either the client or server. + * This gives the extension the ability to modify the message before it is + * processed by Burp. + * + * @param interceptedTextMessage Intercepted text WebSocket message. + * + * @return The {@link TextMessageReceivedAction} containing the required action and text message to be passed through. + */ + TextMessageReceivedAction handleTextMessageReceived(InterceptedTextMessage interceptedTextMessage); + + /** + * Invoked when a text message is about to be sent to either the client or server. + * This gives the extension the ability to modify the message before it is + * sent. + * + * @param interceptedTextMessage Intercepted text WebSocket message. + * + * @return The {@link TextMessageReceivedAction} containing the required action and text message to be passed through. + */ + TextMessageToBeSentAction handleTextMessageToBeSent(InterceptedTextMessage interceptedTextMessage); + + /** + * Invoked when a binary message is received from either the client or server. + * This gives the extension the ability to modify the message before it is + * processed by Burp. + * + * @param interceptedBinaryMessage Intercepted binary WebSocket message. + * + * @return The {@link BinaryMessageReceivedAction} containing the required action and binary message to be passed through. + */ + BinaryMessageReceivedAction handleBinaryMessageReceived(InterceptedBinaryMessage interceptedBinaryMessage); + + /** + * Invoked when a binary message is about to be sent to either the client or server. + * This gives the extension the ability to modify the message before it is + * sent. + * + * @param interceptedBinaryMessage Intercepted binary WebSocket message. + * + * @return The {@link BinaryMessageReceivedAction} containing the required action and binary message to be passed through. + */ + BinaryMessageToBeSentAction handleBinaryMessageToBeSent(InterceptedBinaryMessage interceptedBinaryMessage); + + /** + * Invoked when the WebSocket is closed. + */ + default void onClose() + { + } +} diff --git a/src/burp/api/montoya/proxy/websocket/ProxyWebSocket.java b/src/burp/api/montoya/proxy/websocket/ProxyWebSocket.java new file mode 100644 index 0000000..5a0bc73 --- /dev/null +++ b/src/burp/api/montoya/proxy/websocket/ProxyWebSocket.java @@ -0,0 +1,49 @@ +/* + * Copyright (c) 2022-2023. PortSwigger Ltd. All rights reserved. + * + * This code may be used to extend the functionality of Burp Suite Community Edition + * and Burp Suite Professional, provided that this usage does not violate the + * license terms for those products. + */ + +package burp.api.montoya.proxy.websocket; + +import burp.api.montoya.core.ByteArray; +import burp.api.montoya.core.Registration; +import burp.api.montoya.websocket.Direction; + +/** + * ProxyWebSocket within Burp. + */ +public interface ProxyWebSocket +{ + /** + * This method allows an extension to send a text message via the WebSocket to either the client or the server. + * + * @param textMessage The message to be sent. + * @param direction The direction of the message. + */ + void sendTextMessage(String textMessage, Direction direction); + + /** + * This method allows an extension to send a binary message via the WebSocket to either the client or the server. + * + * @param binaryMessage The message to be sent. + * @param direction The direction of the message. + */ + void sendBinaryMessage(ByteArray binaryMessage, Direction direction); + + /** + * This method will close the WebSocket. + */ + void close(); + + /** + * Register a handler which will perform actions when messages are sent or received by the WebSocket. + * + * @param handler An object created by the extension that implements {@link ProxyMessageHandler} interface. + * + * @return The {@link Registration} for the handler. + */ + Registration registerProxyMessageHandler(ProxyMessageHandler handler); +} diff --git a/src/burp/api/montoya/proxy/websocket/ProxyWebSocketCreation.java b/src/burp/api/montoya/proxy/websocket/ProxyWebSocketCreation.java new file mode 100644 index 0000000..68a8f8b --- /dev/null +++ b/src/burp/api/montoya/proxy/websocket/ProxyWebSocketCreation.java @@ -0,0 +1,27 @@ +/* + * Copyright (c) 2022-2023. PortSwigger Ltd. All rights reserved. + * + * This code may be used to extend the functionality of Burp Suite Community Edition + * and Burp Suite Professional, provided that this usage does not violate the + * license terms for those products. + */ + +package burp.api.montoya.proxy.websocket; + +import burp.api.montoya.http.message.requests.HttpRequest; + +/** + * Information about the proxy web socket that is being created. + */ +public interface ProxyWebSocketCreation +{ + /** + * @return The ProxyWebSocket that is being created. + */ + ProxyWebSocket proxyWebSocket(); + + /** + * @return The HTTP upgrade request that initiated the WebSocket creation. + */ + HttpRequest upgradeRequest(); +} diff --git a/src/burp/api/montoya/proxy/websocket/ProxyWebSocketCreationHandler.java b/src/burp/api/montoya/proxy/websocket/ProxyWebSocketCreationHandler.java new file mode 100644 index 0000000..5801600 --- /dev/null +++ b/src/burp/api/montoya/proxy/websocket/ProxyWebSocketCreationHandler.java @@ -0,0 +1,26 @@ +/* + * Copyright (c) 2022-2023. PortSwigger Ltd. All rights reserved. + * + * This code may be used to extend the functionality of Burp Suite Community Edition + * and Burp Suite Professional, provided that this usage does not violate the + * license terms for those products. + */ + +package burp.api.montoya.proxy.websocket; + +import burp.api.montoya.proxy.Proxy; + +/** + * Extensions can implement this interface and then call {@link Proxy#registerWebSocketCreationHandler} to register a WebSocket handler.
+ * The handler will be notified of new WebSockets being created by the Proxy tool. + */ +public interface ProxyWebSocketCreationHandler +{ + /** + * Invoked by Burp when a WebSocket is being created by the Proxy tool.
+ * Note that the client side of the connection will not be upgraded until after this method completes. + * + * @param webSocketCreation {@link ProxyWebSocketCreation} containing information about the proxy websocket that is being created + */ + void handleWebSocketCreation(ProxyWebSocketCreation webSocketCreation); +} diff --git a/src/burp/api/montoya/proxy/websocket/TextMessageReceivedAction.java b/src/burp/api/montoya/proxy/websocket/TextMessageReceivedAction.java new file mode 100644 index 0000000..9c6f110 --- /dev/null +++ b/src/burp/api/montoya/proxy/websocket/TextMessageReceivedAction.java @@ -0,0 +1,120 @@ +/* + * Copyright (c) 2022-2023. PortSwigger Ltd. All rights reserved. + * + * This code may be used to extend the functionality of Burp Suite Community Edition + * and Burp Suite Professional, provided that this usage does not violate the + * license terms for those products. + */ + +package burp.api.montoya.proxy.websocket; + +import burp.api.montoya.proxy.MessageReceivedAction; +import burp.api.montoya.websocket.TextMessage; + +import static burp.api.montoya.internal.ObjectFactoryLocator.FACTORY; + + +/** + * Extensions can implement this interface when returning a text message from + * {@link ProxyMessageHandler#handleTextMessageReceived(InterceptedTextMessage)}. + */ +public interface TextMessageReceivedAction +{ + /** + * @return The action associated with this message. + */ + MessageReceivedAction action(); + + /** + * @return The payload of this message. + */ + String payload(); + + /** + * Build a text WebSocket message to + * follow the current interception rules to determine the appropriate + * action to take for the message. + * + * @param payload The text message payload. + * + * @return The {@link TextMessageReceivedAction} that allows user rules to be + * followed. + */ + static TextMessageReceivedAction continueWith(String payload) + { + return FACTORY.followUserRulesInitialProxyTextMessage(payload); + } + + /** + * Build a text WebSocket message to + * follow the current interception rules to determine the appropriate + * action to take for the message. + * + * @param message The text message. + * + * @return The {@link TextMessageReceivedAction} that allows user rules to be + * followed. + */ + static TextMessageReceivedAction continueWith(TextMessage message) + { + return FACTORY.followUserRulesInitialProxyTextMessage(message.payload()); + } + + /** + * Build a text WebSocket message to be intercepted within the Proxy. + * + * @param payload The text message payload. + * + * @return The message. + */ + static TextMessageReceivedAction intercept(String payload) + { + return FACTORY.interceptInitialProxyTextMessage(payload); + } + + /** + * Build a text WebSocket message to be intercepted within the Proxy. + * + * @param message The text message. + * + * @return The message. + */ + static TextMessageReceivedAction intercept(TextMessage message) + { + return FACTORY.interceptInitialProxyTextMessage(message.payload()); + } + + /** + * Build a text WebSocket message to continue within the Proxy without interception. + * + * @param payload The text message payload. + * + * @return The message. + */ + static TextMessageReceivedAction doNotIntercept(String payload) + { + return FACTORY.doNotInterceptInitialProxyTextMessage(payload); + } + + /** + * Build a text WebSocket message to continue within the Proxy without interception. + * + * @param message The text message payload. + * + * @return The message. + */ + static TextMessageReceivedAction doNotIntercept(TextMessage message) + { + return FACTORY.doNotInterceptInitialProxyTextMessage(message.payload()); + } + + /** + * Build a text WebSocket message to be dropped. + * + * @return The message to be dropped. + */ + static TextMessageReceivedAction drop() + { + return FACTORY.dropInitialProxyTextMessage(); + } +} diff --git a/src/burp/api/montoya/proxy/websocket/TextMessageToBeSentAction.java b/src/burp/api/montoya/proxy/websocket/TextMessageToBeSentAction.java new file mode 100644 index 0000000..12f5cd6 --- /dev/null +++ b/src/burp/api/montoya/proxy/websocket/TextMessageToBeSentAction.java @@ -0,0 +1,66 @@ +/* + * Copyright (c) 2022-2023. PortSwigger Ltd. All rights reserved. + * + * This code may be used to extend the functionality of Burp Suite Community Edition + * and Burp Suite Professional, provided that this usage does not violate the + * license terms for those products. + */ + +package burp.api.montoya.proxy.websocket; + +import burp.api.montoya.proxy.MessageToBeSentAction; +import burp.api.montoya.websocket.TextMessage; + +import static burp.api.montoya.internal.ObjectFactoryLocator.FACTORY; + + +/** + * Extensions can implement this interface when returning a text message from + * {@link ProxyMessageHandler#handleTextMessageToBeSent(InterceptedTextMessage)}. + */ +public interface TextMessageToBeSentAction +{ + /** + * @return The action associated with this message. + */ + MessageToBeSentAction action(); + + /** + * @return The payload of this message. + */ + String payload(); + + /** + * Build a text WebSocket message to continue through Burp. + * + * @param payload The text message payload. + * + * @return The message. + */ + static TextMessageToBeSentAction continueWith(String payload) + { + return FACTORY.continueWithFinalProxyTextMessage(payload); + } + + /** + * Build a text WebSocket message to continue through Burp. + * + * @param message The text message. + * + * @return The message. + */ + static TextMessageToBeSentAction continueWith(TextMessage message) + { + return FACTORY.continueWithFinalProxyTextMessage(message.payload()); + } + + /** + * Build a text WebSocket message to be dropped. + * + * @return The message to be dropped. + */ + static TextMessageToBeSentAction drop() + { + return FACTORY.dropFinalProxyTextMessage(); + } +} diff --git a/src/burp/api/montoya/repeater/Repeater.java b/src/burp/api/montoya/repeater/Repeater.java new file mode 100644 index 0000000..aaaf1c2 --- /dev/null +++ b/src/burp/api/montoya/repeater/Repeater.java @@ -0,0 +1,39 @@ +/* + * Copyright (c) 2022-2023. PortSwigger Ltd. All rights reserved. + * + * This code may be used to extend the functionality of Burp Suite Community Edition + * and Burp Suite Professional, provided that this usage does not violate the + * license terms for those products. + */ + +package burp.api.montoya.repeater; + +import burp.api.montoya.http.message.requests.HttpRequest; + +/** + * Provides access to the functionality of the Repeater tool. + */ +public interface Repeater +{ + /** + * This method can be used to send an HTTP request to the Burp Repeater + * tool. The request will be displayed in the user interface using a + * default tab index, but will not be sent until the user initiates + * this action. + * + * @param request The full HTTP request. + */ + void sendToRepeater(HttpRequest request); + + /** + * This method can be used to send an HTTP request to the Burp Repeater + * tool. The request will be displayed in the user interface, but will not + * be issued until the user initiates this action. + * + * @param request The full HTTP request. + * @param name An optional caption which will appear on the Repeater + * tab containing the request. If this value is {@code null} then a default + * tab index will be displayed. + */ + void sendToRepeater(HttpRequest request, String name); +} diff --git a/src/burp/api/montoya/scanner/AuditConfiguration.java b/src/burp/api/montoya/scanner/AuditConfiguration.java new file mode 100644 index 0000000..32cdd90 --- /dev/null +++ b/src/burp/api/montoya/scanner/AuditConfiguration.java @@ -0,0 +1,29 @@ +/* + * Copyright (c) 2022-2023. PortSwigger Ltd. All rights reserved. + * + * This code may be used to extend the functionality of Burp Suite Community Edition + * and Burp Suite Professional, provided that this usage does not violate the + * license terms for those products. + */ + +package burp.api.montoya.scanner; + +import static burp.api.montoya.internal.ObjectFactoryLocator.FACTORY; + +/** + * This class represents the configuration required for an audit in the Burp Scanner Tool. + */ +public interface AuditConfiguration +{ + /** + * This method can be used to create a built-in audit configuration. + * + * @param configuration The {@link BuiltInAuditConfiguration} to use for the audit. + * + * @return a {@code AuditConfiguration} based on a built-in configuration + */ + static AuditConfiguration auditConfiguration(BuiltInAuditConfiguration configuration) + { + return FACTORY.auditConfiguration(configuration); + } +} diff --git a/src/burp/api/montoya/scanner/AuditResult.java b/src/burp/api/montoya/scanner/AuditResult.java new file mode 100644 index 0000000..89487e3 --- /dev/null +++ b/src/burp/api/montoya/scanner/AuditResult.java @@ -0,0 +1,30 @@ +/* + * Copyright (c) 2022-2023. PortSwigger Ltd. All rights reserved. + * + * This code may be used to extend the functionality of Burp Suite Community Edition + * and Burp Suite Professional, provided that this usage does not violate the + * license terms for those products. + */ + +package burp.api.montoya.scanner; + +import burp.api.montoya.scanner.audit.issues.AuditIssue; + +import java.util.List; + +import static burp.api.montoya.internal.ObjectFactoryLocator.FACTORY; + +public interface AuditResult +{ + List auditIssues(); + + static AuditResult auditResult(List auditIssues) + { + return FACTORY.auditResult(auditIssues); + } + + static AuditResult auditResult(AuditIssue... auditIssues) + { + return FACTORY.auditResult(auditIssues); + } +} diff --git a/src/burp/api/montoya/scanner/BuiltInAuditConfiguration.java b/src/burp/api/montoya/scanner/BuiltInAuditConfiguration.java new file mode 100644 index 0000000..cf401e4 --- /dev/null +++ b/src/burp/api/montoya/scanner/BuiltInAuditConfiguration.java @@ -0,0 +1,18 @@ +/* + * Copyright (c) 2022-2023. PortSwigger Ltd. All rights reserved. + * + * This code may be used to extend the functionality of Burp Suite Community Edition + * and Burp Suite Professional, provided that this usage does not violate the + * license terms for those products. + */ + +package burp.api.montoya.scanner; + +/** + * This enum represents built in configurations for the Burp Scanner tool. + */ +public enum BuiltInAuditConfiguration +{ + LEGACY_PASSIVE_AUDIT_CHECKS, + LEGACY_ACTIVE_AUDIT_CHECKS +} diff --git a/src/burp/api/montoya/scanner/ConsolidationAction.java b/src/burp/api/montoya/scanner/ConsolidationAction.java new file mode 100644 index 0000000..ac4b492 --- /dev/null +++ b/src/burp/api/montoya/scanner/ConsolidationAction.java @@ -0,0 +1,20 @@ +/* + * Copyright (c) 2022-2023. PortSwigger Ltd. All rights reserved. + * + * This code may be used to extend the functionality of Burp Suite Community Edition + * and Burp Suite Professional, provided that this usage does not violate the + * license terms for those products. + */ + +package burp.api.montoya.scanner; + +/** + * This enum represents the action to be taken when duplicate audit issues are + * found. + */ +public enum ConsolidationAction +{ + KEEP_EXISTING, + KEEP_BOTH, + KEEP_NEW +} diff --git a/src/burp/api/montoya/scanner/Crawl.java b/src/burp/api/montoya/scanner/Crawl.java new file mode 100644 index 0000000..072b5bf --- /dev/null +++ b/src/burp/api/montoya/scanner/Crawl.java @@ -0,0 +1,48 @@ +/* + * Copyright (c) 2022-2023. PortSwigger Ltd. All rights reserved. + * + * This code may be used to extend the functionality of Burp Suite Community Edition + * and Burp Suite Professional, provided that this usage does not violate the + * license terms for those products. + */ + +package burp.api.montoya.scanner; + +/** + * Crawl in the Burp Scanner tool. + */ +public interface Crawl extends ScanTask +{ + /** + * Number of requests that have been made for the + * scan task. + * + * @return The number of requests that have been made for the scan task. + */ + @Override + int requestCount(); + + /** + * Number of network errors that have occurred for + * the scan task. + * + * @return The number of network errors that have occurred for the scan + * task. + */ + @Override + int errorCount(); + + /** + * Delete the task. + */ + @Override + void delete(); + + /** + * This functionality is not yet implemented. + * + * @return the current status message of the task + */ + @Override + String statusMessage(); +} diff --git a/src/burp/api/montoya/scanner/CrawlAndAudit.java b/src/burp/api/montoya/scanner/CrawlAndAudit.java new file mode 100644 index 0000000..8675186 --- /dev/null +++ b/src/burp/api/montoya/scanner/CrawlAndAudit.java @@ -0,0 +1,46 @@ +/* + * Copyright (c) 2022-2023. PortSwigger Ltd. All rights reserved. + * + * This code may be used to extend the functionality of Burp Suite Community Edition + * and Burp Suite Professional, provided that this usage does not violate the + * license terms for those products. + */ + +package burp.api.montoya.scanner; + +/** + * Crawl and audit in the Burp Scanner tool. + */ +public interface CrawlAndAudit extends ScanTask +{ + /** + * Number of requests that have been made for the + * scan task. + * + * @return The number of requests that have been made for the scan task. + */ + @Override + int requestCount(); + + /** + * Number of network errors that have occurred for + * the scan task. + * + * @return The number of network errors that have occurred for the scan + * task. + */ + @Override + int errorCount(); + + /** + * Delete the task. + */ + @Override + void delete(); + + /** + * @return the current status message of the task + */ + @Override + String statusMessage(); +} diff --git a/src/burp/api/montoya/scanner/CrawlConfiguration.java b/src/burp/api/montoya/scanner/CrawlConfiguration.java new file mode 100644 index 0000000..8b721b9 --- /dev/null +++ b/src/burp/api/montoya/scanner/CrawlConfiguration.java @@ -0,0 +1,36 @@ +/* + * Copyright (c) 2022-2023. PortSwigger Ltd. All rights reserved. + * + * This code may be used to extend the functionality of Burp Suite Community Edition + * and Burp Suite Professional, provided that this usage does not violate the + * license terms for those products. + */ + +package burp.api.montoya.scanner; + +import java.util.List; + +import static burp.api.montoya.internal.ObjectFactoryLocator.FACTORY; + +/** + * This class represents the configuration required for an crawl in the Burp Scanner Tool. + */ +public interface CrawlConfiguration +{ + /** + * @return the seed urls for the crawl + */ + List seedUrls(); + + /** + * Build a crawl configuration with seed urls + * + * @param seedUrls used by the crawler + * + * @return crawl configuration required by the crawler. + */ + static CrawlConfiguration crawlConfiguration(String... seedUrls) + { + return FACTORY.crawlConfiguration(seedUrls); + } +} diff --git a/src/burp/api/montoya/scanner/ReportFormat.java b/src/burp/api/montoya/scanner/ReportFormat.java new file mode 100644 index 0000000..5e8053b --- /dev/null +++ b/src/burp/api/montoya/scanner/ReportFormat.java @@ -0,0 +1,18 @@ +/* + * Copyright (c) 2022-2023. PortSwigger Ltd. All rights reserved. + * + * This code may be used to extend the functionality of Burp Suite Community Edition + * and Burp Suite Professional, provided that this usage does not violate the + * license terms for those products. + */ + +package burp.api.montoya.scanner; + +/** + * This enum represents the formats for scan reports. + */ +public enum ReportFormat +{ + HTML, + XML +} \ No newline at end of file diff --git a/src/burp/api/montoya/scanner/ScanCheck.java b/src/burp/api/montoya/scanner/ScanCheck.java new file mode 100644 index 0000000..adb5a9a --- /dev/null +++ b/src/burp/api/montoya/scanner/ScanCheck.java @@ -0,0 +1,80 @@ +/* + * Copyright (c) 2022-2023. PortSwigger Ltd. All rights reserved. + * + * This code may be used to extend the functionality of Burp Suite Community Edition + * and Burp Suite Professional, provided that this usage does not violate the + * license terms for those products. + */ + +package burp.api.montoya.scanner; + +import burp.api.montoya.http.message.HttpRequestResponse; +import burp.api.montoya.scanner.audit.insertionpoint.AuditInsertionPoint; +import burp.api.montoya.scanner.audit.issues.AuditIssue; + +/** + * Extensions can implement this interface and then call + * {@link Scanner#registerScanCheck(ScanCheck)} to register a custom Scanner + * check. When performing an audit, Burp will ask the check to perform an + * active or passive audit on the base request, and report any audit issues + * that are identified. + */ +public interface ScanCheck +{ + /** + * The Scanner invokes this method for each insertion point that is + * actively audited. Extensions may issue HTTP requests as required to + * carry out an active audit, and should use the + * {@link AuditInsertionPoint} object provided to build requests for + * particular payloads. + * Note: + * Scan checks should submit raw non-encoded payloads to insertion points, + * and the insertion point has responsibility for performing any data + * encoding that is necessary given the nature and location of the insertion + * point. + * + * @param baseRequestResponse The base {@link HttpRequestResponse} that + * should be actively audited. + * @param auditInsertionPoint An {@link AuditInsertionPoint} object that + * can be queried to obtain details of the insertion point being tested, and + * can be used to build requests for particular payloads. + * + * @return An {@link AuditResult} object with a list of {@link AuditIssue} + * objects, or an empty {@link AuditResult} object if no issues are identified. + */ + AuditResult activeAudit(HttpRequestResponse baseRequestResponse, AuditInsertionPoint auditInsertionPoint); + + /** + * The Scanner invokes this method for each base request / response that is + * passively audited. Note: Extensions should only analyze the + * HTTP messages provided during a passive audit, and should not make any + * new HTTP requests of their own. + * + * @param baseRequestResponse The base {@link HttpRequestResponse} that + * should be passively audited. + * + * @return An {@link AuditResult} object with a list of {@link AuditIssue} + * objects, or an empty {@link AuditResult} object if no issues are identified. + */ + AuditResult passiveAudit(HttpRequestResponse baseRequestResponse); + + /** + * The Scanner invokes this method when the custom Scan check has + * reported multiple issues for the same URL path. This can arise either + * because there are multiple distinct vulnerabilities, or because the same + * (or a similar) request has been scanned more than once. The custom check + * should determine whether the issues are duplicates. In most cases, where + * a check uses distinct issue names or descriptions for distinct issues, + * the consolidation process will simply be a matter of comparing these + * features for the two issues. + * + * @param newIssue An {@link AuditIssue} at the same URL path that has been + * newly reported by this Scan check. + * @param existingIssue An {@link AuditIssue} that was previously reported + * by this Scan check. + * + * @return A {@link ConsolidationAction} to determine which issue(s) should + * be reported in the main Scanner results. + */ + ConsolidationAction consolidateIssues(AuditIssue newIssue, AuditIssue existingIssue); +} diff --git a/src/burp/api/montoya/scanner/ScanConfiguration.java b/src/burp/api/montoya/scanner/ScanConfiguration.java new file mode 100644 index 0000000..1f35044 --- /dev/null +++ b/src/burp/api/montoya/scanner/ScanConfiguration.java @@ -0,0 +1,16 @@ +/* + * Copyright (c) 2022-2023. PortSwigger Ltd. All rights reserved. + * + * This code may be used to extend the functionality of Burp Suite Community Edition + * and Burp Suite Professional, provided that this usage does not violate the + * license terms for those products. + */ + +package burp.api.montoya.scanner; + +/** + * Configurations for the Burp Scanner tool. + */ +public interface ScanConfiguration +{ +} diff --git a/src/burp/api/montoya/scanner/ScanTask.java b/src/burp/api/montoya/scanner/ScanTask.java new file mode 100644 index 0000000..8d05be9 --- /dev/null +++ b/src/burp/api/montoya/scanner/ScanTask.java @@ -0,0 +1,46 @@ +/* + * Copyright (c) 2022-2023. PortSwigger Ltd. All rights reserved. + * + * This code may be used to extend the functionality of Burp Suite Community Edition + * and Burp Suite Professional, provided that this usage does not violate the + * license terms for those products. + */ + +package burp.api.montoya.scanner; + +import burp.api.montoya.core.Task; + +/** + * This interface is used to retrieve details of tasks in the Burp Scanner. + */ +public interface ScanTask extends Task +{ + /** + * Number of requests that have been made for the + * scan task. + * + * @return The number of requests that have been made for the scan task. + */ + int requestCount(); + + /** + * Number of network errors that have occurred for + * the scan task. + * + * @return The number of network errors that have occurred for the scan + * task. + */ + int errorCount(); + + /** + * Delete the task. + */ + @Override + void delete(); + + /** + * @return the current status message of the task + */ + @Override + String statusMessage(); +} diff --git a/src/burp/api/montoya/scanner/Scanner.java b/src/burp/api/montoya/scanner/Scanner.java new file mode 100644 index 0000000..878bc77 --- /dev/null +++ b/src/burp/api/montoya/scanner/Scanner.java @@ -0,0 +1,96 @@ +/* + * Copyright (c) 2022-2023. PortSwigger Ltd. All rights reserved. + * + * This code may be used to extend the functionality of Burp Suite Community Edition + * and Burp Suite Professional, provided that this usage does not violate the + * license terms for those products. + */ + +package burp.api.montoya.scanner; + +import burp.api.montoya.core.Registration; +import burp.api.montoya.scanner.audit.Audit; +import burp.api.montoya.scanner.audit.AuditIssueHandler; +import burp.api.montoya.scanner.audit.insertionpoint.AuditInsertionPointProvider; +import burp.api.montoya.scanner.audit.issues.AuditIssue; +import burp.api.montoya.scanner.bchecks.BChecks; + +import java.nio.file.Path; +import java.util.List; + +/** + * [Professional only] Provides access to the functionality of the Scanner tool. + */ +public interface Scanner +{ + /** + * Register a handler which will be notified of new + * audit issues that are reported by the Scanner tool. Extensions can + * perform custom analysis or logging of audit issues by registering an + * audit issue handler. + * + * @param auditIssueHandler An object created by the extension that + * implements the {@link AuditIssueHandler} interface. + * + * @return The {@link Registration} for the handler. + */ + Registration registerAuditIssueHandler(AuditIssueHandler auditIssueHandler); + + /** + * Register a custom Scanner check. When performing + * scanning, Burp will ask the check to perform active or passive scanning + * on the base request, and report any Scanner issues that are identified. + * + * @param scanCheck An object created by the extension that implements the + * {@link ScanCheck} interface. + * + * @return The {@link Registration} for the check. + */ + Registration registerScanCheck(ScanCheck scanCheck); + + /** + * Register a provider of Scanner insertion points. + * For each base request that is actively scanned, Burp will ask the + * provider to provide any custom Scanner insertion points that are + * appropriate for the request. + * + * @param insertionPointProvider An object created by the extension that + * implements the {@link AuditInsertionPointProvider} interface. + * + * @return The {@link Registration} for the provider. + */ + Registration registerInsertionPointProvider(AuditInsertionPointProvider insertionPointProvider); + + /** + * This method can be used to start a crawl in the Burp Scanner tool. + * + * @return The {@link Crawl} started in the Burp Scanner tool. + */ + Crawl startCrawl(CrawlConfiguration crawlConfiguration); + + /** + * This method can be used to start an audit in the Burp Scanner tool. + * + * @return The {@link Audit} started in the Burp Scanner tool. + */ + Audit startAudit(AuditConfiguration auditConfiguration); + + /** + * Generate a report for the specified Scanner + * issues. The report format can be specified. For all other reporting + * options, the default settings that appear in the reporting UI wizard are + * used. + * + * @param issues The {@link AuditIssue}s issues to be reported. + * @param format The {@link ReportFormat} to be used in the report. + * @param path The {@link Path} to the file that will be saved. + */ + void generateReport(List issues, ReportFormat format, Path path); + + /** + * Access functionality related to BChecks. + * + * @return An implementation of the {@link BChecks} interface which exposes BChecks functionality. + */ + BChecks bChecks(); +} diff --git a/src/burp/api/montoya/scanner/audit/Audit.java b/src/burp/api/montoya/scanner/audit/Audit.java new file mode 100644 index 0000000..e2cedb4 --- /dev/null +++ b/src/burp/api/montoya/scanner/audit/Audit.java @@ -0,0 +1,93 @@ +/* + * Copyright (c) 2022-2023. PortSwigger Ltd. All rights reserved. + * + * This code may be used to extend the functionality of Burp Suite Community Edition + * and Burp Suite Professional, provided that this usage does not violate the + * license terms for those products. + */ + +package burp.api.montoya.scanner.audit; + +import burp.api.montoya.core.Range; +import burp.api.montoya.http.message.HttpRequestResponse; +import burp.api.montoya.http.message.requests.HttpRequest; +import burp.api.montoya.scanner.ScanTask; +import burp.api.montoya.scanner.audit.issues.AuditIssue; + +import java.util.List; + +/** + * Audit in the Burp Scanner tool. + */ +public interface Audit extends ScanTask +{ + /** + * This method retrieves the number of insertion points. + * + * @return The number of insertion points. + */ + int insertionPointCount(); + + /** + * This method retrieves the audit issues found by this audit. + * + * @return The list of {@link AuditIssue}s found by this audit. + */ + List issues(); + + /** + * This method can be used to add an HTTP request to this audit. + * + * @param request The {@link HttpRequest} to add to this audit. + */ + void addRequest(HttpRequest request); + + /** + * This method can be used to add an HTTP request to this audit. + * + * @param request The {@link HttpRequest} to add to this audit. + * @param insertionPointOffsets The list of {@link Range}s representing the + * insertion point offsets. + */ + void addRequest(HttpRequest request, List insertionPointOffsets); + + /** + * This method can be used to add an HTTP request and response to this + * audit. + * + * @param requestResponse The {@link HttpRequestResponse} to add to this + * audit. + */ + void addRequestResponse(HttpRequestResponse requestResponse); + + /** + * Number of requests that have been made for the + * scan task. + * + * @return The number of requests that have been made for the scan task. + */ + @Override + int requestCount(); + + /** + * Number of network errors that have occurred for + * the scan task. + * + * @return The number of network errors that have occurred for the scan + * task. + */ + @Override + int errorCount(); + + /** + * Delete the task. + */ + @Override + void delete(); + + /** + * @return the current status message of the task + */ + @Override + String statusMessage(); +} diff --git a/src/burp/api/montoya/scanner/audit/AuditIssueHandler.java b/src/burp/api/montoya/scanner/audit/AuditIssueHandler.java new file mode 100644 index 0000000..7a01fc3 --- /dev/null +++ b/src/burp/api/montoya/scanner/audit/AuditIssueHandler.java @@ -0,0 +1,31 @@ +/* + * Copyright (c) 2022-2023. PortSwigger Ltd. All rights reserved. + * + * This code may be used to extend the functionality of Burp Suite Community Edition + * and Burp Suite Professional, provided that this usage does not violate the + * license terms for those products. + */ + +package burp.api.montoya.scanner.audit; + +import burp.api.montoya.scanner.Scanner; +import burp.api.montoya.scanner.audit.issues.AuditIssue; + +/** + * Extensions can implement this interface and then call + * {@link Scanner#registerAuditIssueHandler(AuditIssueHandler)} to register an + * audit issue handler. The handler will be notified of new issues that are + * reported by the Scanner tool. Extensions can perform custom analysis or + * logging of audit issues by registering an audit issue handler. + */ +public interface AuditIssueHandler +{ + /** + * This method is invoked when a new issue is added to Burp Scanner's + * results. + * + * @param auditIssue An {@link AuditIssue} object that the extension can + * query to obtain details about the new issue. + */ + void handleNewAuditIssue(AuditIssue auditIssue); +} diff --git a/src/burp/api/montoya/scanner/audit/insertionpoint/AuditInsertionPoint.java b/src/burp/api/montoya/scanner/audit/insertionpoint/AuditInsertionPoint.java new file mode 100644 index 0000000..96b31d9 --- /dev/null +++ b/src/burp/api/montoya/scanner/audit/insertionpoint/AuditInsertionPoint.java @@ -0,0 +1,106 @@ +/* + * Copyright (c) 2022-2023. PortSwigger Ltd. All rights reserved. + * + * This code may be used to extend the functionality of Burp Suite Community Edition + * and Burp Suite Professional, provided that this usage does not violate the + * license terms for those products. + */ + +package burp.api.montoya.scanner.audit.insertionpoint; + +import burp.api.montoya.core.ByteArray; +import burp.api.montoya.core.Range; +import burp.api.montoya.http.message.requests.HttpRequest; +import burp.api.montoya.scanner.ScanCheck; + +import java.util.List; + +import static burp.api.montoya.internal.ObjectFactoryLocator.FACTORY; + +/** + * This interface is used to define an insertion point for use by active Scan + * checks. Extensions can obtain instances of this interface by registering an + * {@link ScanCheck}, or can create instances for use by Burp's own scan checks + * by registering an {@link AuditInsertionPointProvider}. + */ +public interface AuditInsertionPoint +{ + /** + * Name of this insertion point. + * + * @return The name of this insertion point (for example, a description of + * a particular request parameter). + */ + String name(); + + /** + * Base value for this insertion point. + * + * @return the base value that appears in this insertion point in the base + * request being audited, or {@code null} if there is no value in the base + * request that corresponds to this insertion point. + */ + String baseValue(); + + /** + * Build a request with the specified payload placed + * into the insertion point. There is no requirement for extension-provided + * insertion points to adjust the Content-Length header in requests if the + * body length has changed, although Burp-provided insertion points will + * always do this and will return a request with a valid Content-Length + * header. + * Note: + * Scan checks should submit raw non-encoded payloads to insertion points, + * and the insertion point has responsibility for performing any data + * encoding that is necessary given the nature and location of the insertion + * point. + * + * @param payload The payload that should be placed into the insertion + * point. + * + * @return The resulting request. + */ + HttpRequest buildHttpRequestWithPayload(ByteArray payload); + + /** + * Determine the offsets of the payload value within + * the request, when it is placed into the insertion point. Scan checks may + * invoke this method when reporting issues, so as to highlight the + * relevant part of the request within the UI. + * + * @param payload The payload that should be placed into the insertion + * point. + * + * @return A list of {@link Range} objects containing the start and end + * offsets of the payload within the request, or an empty list if this is + * not applicable (for example, where the insertion point places a payload + * into a serialized data structure, the raw payload may not literally + * appear anywhere within the resulting request). + */ + List issueHighlights(ByteArray payload); + + /** + * Type of this insertion point. + * + * @return The {@link AuditInsertionPointType} for this insertion point. + */ + default AuditInsertionPointType type() + { + return AuditInsertionPointType.EXTENSION_PROVIDED; + } + + /** + * This method can be used to create an audit insertion point based on offsets. + * + * @param name The name of the audit insertion point. + * @param baseRequest The base {@link HttpRequest}. + * @param startIndexInclusive The start index inclusive. + * @param endIndexExclusive The end index exclusive. + * + * @return The {@link AuditInsertionPoint} based on offsets. + */ + static AuditInsertionPoint auditInsertionPoint(String name, HttpRequest baseRequest, int startIndexInclusive, int endIndexExclusive) + { + return FACTORY.auditInsertionPoint(name, baseRequest, startIndexInclusive, endIndexExclusive); + } +} diff --git a/src/burp/api/montoya/scanner/audit/insertionpoint/AuditInsertionPointProvider.java b/src/burp/api/montoya/scanner/audit/insertionpoint/AuditInsertionPointProvider.java new file mode 100644 index 0000000..439cdce --- /dev/null +++ b/src/burp/api/montoya/scanner/audit/insertionpoint/AuditInsertionPointProvider.java @@ -0,0 +1,38 @@ +/* + * Copyright (c) 2022-2023. PortSwigger Ltd. All rights reserved. + * + * This code may be used to extend the functionality of Burp Suite Community Edition + * and Burp Suite Professional, provided that this usage does not violate the + * license terms for those products. + */ + +package burp.api.montoya.scanner.audit.insertionpoint; + +import burp.api.montoya.http.message.HttpRequestResponse; +import burp.api.montoya.scanner.Scanner; + +import java.util.List; + +/** + * Extensions can implement this interface and then call + * {@link Scanner#registerInsertionPointProvider(AuditInsertionPointProvider)} + * to register a provider for custom audit insertion points. + */ +public interface AuditInsertionPointProvider +{ + /** + * The Scanner invokes this method when a request is actively audited. The + * provider should provide a list of custom insertion points that + * will be used in the audit. Note: these insertion points are used + * in addition to those that are derived from Burp Scanner's configuration, + * and those provided by any other Burp extensions. + * + * @param baseHttpRequestResponse The base {@link HttpRequestResponse} that + * will be actively audited. + * + * @return A list of {@link AuditInsertionPoint} objects + * that should be used in the audit, or {@code null} if no custom insertion + * points are applicable for this request. + */ + List provideInsertionPoints(HttpRequestResponse baseHttpRequestResponse); +} diff --git a/src/burp/api/montoya/scanner/audit/insertionpoint/AuditInsertionPointType.java b/src/burp/api/montoya/scanner/audit/insertionpoint/AuditInsertionPointType.java new file mode 100644 index 0000000..2db2c3c --- /dev/null +++ b/src/burp/api/montoya/scanner/audit/insertionpoint/AuditInsertionPointType.java @@ -0,0 +1,33 @@ +/* + * Copyright (c) 2022-2023. PortSwigger Ltd. All rights reserved. + * + * This code may be used to extend the functionality of Burp Suite Community Edition + * and Burp Suite Professional, provided that this usage does not violate the + * license terms for those products. + */ + +package burp.api.montoya.scanner.audit.insertionpoint; + +/** + * This enum represents the audit insertion point type. + */ +public enum AuditInsertionPointType +{ + PARAM_URL, + PARAM_BODY, + PARAM_COOKIE, + PARAM_XML, + PARAM_XML_ATTR, + PARAM_MULTIPART_ATTR, + PARAM_JSON, + PARAM_AMF, + HEADER, + PARAM_NAME_URL, + PARAM_NAME_BODY, + ENTIRE_BODY, + URL_PATH_FILENAME, + URL_PATH_FOLDER, + USER_PROVIDED, + EXTENSION_PROVIDED, + UNKNOWN +} diff --git a/src/burp/api/montoya/scanner/audit/issues/AuditIssue.java b/src/burp/api/montoya/scanner/audit/issues/AuditIssue.java new file mode 100644 index 0000000..2bf3958 --- /dev/null +++ b/src/burp/api/montoya/scanner/audit/issues/AuditIssue.java @@ -0,0 +1,179 @@ +/* + * Copyright (c) 2022-2023. PortSwigger Ltd. All rights reserved. + * + * This code may be used to extend the functionality of Burp Suite Community Edition + * and Burp Suite Professional, provided that this usage does not violate the + * license terms for those products. + */ + +package burp.api.montoya.scanner.audit.issues; + +import burp.api.montoya.collaborator.Interaction; +import burp.api.montoya.http.HttpService; +import burp.api.montoya.http.message.HttpRequestResponse; +import burp.api.montoya.scanner.ScanCheck; +import burp.api.montoya.scanner.audit.AuditIssueHandler; +import burp.api.montoya.sitemap.SiteMap; + +import java.util.List; + +import static burp.api.montoya.internal.ObjectFactoryLocator.FACTORY; + +/** + * This interface is used to retrieve details of audit issues. Extensions can + * obtain details of issues by registering an {@link AuditIssueHandler}. + * Extensions can also add custom audit issues by registering an + * {@link ScanCheck} or calling {@link SiteMap#add(AuditIssue)}, + * and providing their own implementations of this interface. Note that issue + * descriptions and other text generated by extensions are subject to an HTML + * whitelist that allows only formatting tags and simple hyperlinks. + */ +public interface AuditIssue +{ + /** + * Name of this issue type. + * + * @return The name of this issue type (e.g. "SQL injection"). + */ + String name(); + + /** + * This method returns detailed information about this specific instance of + * the issue. + * + * @return Detailed information about this specific instance of the issue, + * or {@code null} if none applies. A limited set of HTML tags may be used. + */ + String detail(); + + /** + * This method returns detailed information about the remediation for this + * specific instance of the issue. + * + * @return Detailed information about the remediation for this specific + * instance of the issue, or {@code null} if none applies. A limited set of + * HTML tags may be used. + */ + String remediation(); + + /** + * HTTP service for which the issue was generated. + * + * @return The HTTP service for which the issue was generated. + */ + HttpService httpService(); + + /** + * Base URL for which this issue was generated. + * + * @return The base URL for which this issue was generated. + */ + String baseUrl(); + + /** + * Issue severity level. + * + * @return The {@link AuditIssueSeverity} level. + */ + AuditIssueSeverity severity(); + + /** + * Issue confidence level. + * + * @return The {@link AuditIssueConfidence} level. + */ + AuditIssueConfidence confidence(); + + /** + * HTTP request/response messages that caused the issue to be generated. + * + * @return The list of {@link HttpRequestResponse} objects on the basis of + * which the issue was generated. + */ + List requestResponses(); + + /** + * Collaborator interactions that caused the issue to be generated. + * + * @return The list of Burp Collaborator {@link Interaction} objects that caused the issue to be generated. + * If there are no interactions, this will be empty. + */ + List collaboratorInteractions(); + + /** + * Definition for this issue. + * + * @return The {@link AuditIssueDefinition} for this issue. + */ + AuditIssueDefinition definition(); + + /** + * This method can be used to create a default implementation of an audit + * issue for a URL. + * + * @param name The name of the issue type. + * @param detail The detailed information about the issue. + * @param remediation The detailed information about the remediation for + * the issue. + * @param baseUrl The base URL for which the issue is generated. + * @param severity The {@link AuditIssueSeverity} level. + * @param confidence The {@link AuditIssueConfidence} level. + * @param background The background description for the type of issue. + * @param remediationBackground The background description of the + * remediation for this type of issue. + * @param typicalSeverity The typical {@link AuditIssueSeverity} level. + * @param requestResponses The {@link HttpRequestResponse} objects on the + * basis of which the issue is generated. + * + * @return The audit issue for the URL. + */ + static AuditIssue auditIssue( + String name, + String detail, + String remediation, + String baseUrl, + AuditIssueSeverity severity, + AuditIssueConfidence confidence, + String background, + String remediationBackground, + AuditIssueSeverity typicalSeverity, + HttpRequestResponse... requestResponses) + { + return FACTORY.auditIssue(name, detail, remediation, baseUrl, severity, confidence, background, remediationBackground, typicalSeverity, requestResponses); + } + + /** + * This method can be used to create a default implementation of an audit + * issue for a URL. + * + * @param name The name of the issue type. + * @param detail The detailed information about the issue. + * @param remediation The detailed information about the remediation for + * the issue. + * @param baseUrl The base URL for which the issue is generated. + * @param severity The {@link AuditIssueSeverity} level. + * @param confidence The {@link AuditIssueConfidence} level. + * @param background The background description for the type of issue. + * @param remediationBackground The background description of the + * remediation for this type of issue. + * @param typicalSeverity The typical {@link AuditIssueSeverity} level. + * @param requestResponses The list of {@link HttpRequestResponse} objects + * on the basis of which the issue is generated. + * + * @return The audit issue for the URL. + */ + static AuditIssue auditIssue( + String name, + String detail, + String remediation, + String baseUrl, + AuditIssueSeverity severity, + AuditIssueConfidence confidence, + String background, + String remediationBackground, + AuditIssueSeverity typicalSeverity, + List requestResponses) + { + return FACTORY.auditIssue(name, detail, remediation, baseUrl, severity, confidence, background, remediationBackground, typicalSeverity, requestResponses); + } +} diff --git a/src/burp/api/montoya/scanner/audit/issues/AuditIssueConfidence.java b/src/burp/api/montoya/scanner/audit/issues/AuditIssueConfidence.java new file mode 100644 index 0000000..d1486c6 --- /dev/null +++ b/src/burp/api/montoya/scanner/audit/issues/AuditIssueConfidence.java @@ -0,0 +1,19 @@ +/* + * Copyright (c) 2022-2023. PortSwigger Ltd. All rights reserved. + * + * This code may be used to extend the functionality of Burp Suite Community Edition + * and Burp Suite Professional, provided that this usage does not violate the + * license terms for those products. + */ + +package burp.api.montoya.scanner.audit.issues; + +/** + * This enum represents the confidence level of an audit issue. + */ +public enum AuditIssueConfidence +{ + CERTAIN, + FIRM, + TENTATIVE +} diff --git a/src/burp/api/montoya/scanner/audit/issues/AuditIssueDefinition.java b/src/burp/api/montoya/scanner/audit/issues/AuditIssueDefinition.java new file mode 100644 index 0000000..d3e3c37 --- /dev/null +++ b/src/burp/api/montoya/scanner/audit/issues/AuditIssueDefinition.java @@ -0,0 +1,77 @@ +/* + * Copyright (c) 2022-2023. PortSwigger Ltd. All rights reserved. + * + * This code may be used to extend the functionality of Burp Suite Community Edition + * and Burp Suite Professional, provided that this usage does not violate the + * license terms for those products. + */ + +package burp.api.montoya.scanner.audit.issues; + +import static burp.api.montoya.internal.ObjectFactoryLocator.FACTORY; + +/** + * This interface is used to retrieve background information about audit + * issues. Note that text generated by extensions is subject to an HTML + * whitelist that allows only formatting tags and simple hyperlinks. + */ +public interface AuditIssueDefinition +{ + + /** + * Name of this issue type. + * + * @return The name of this issue type (e.g. "SQL injection"). + */ + String name(); + + /** + * This method returns a background description for this issue type. + * + * @return A background description for this type of issue, or {@code null} + * if none applies. A limited set of HTML tags may be used. + */ + String background(); + + /** + * This method returns a background description of the remediation for this + * type of issue. + * + * @return A background description of the remediation for this type of + * issue, or {@code null} if none applies. A limited set of HTML tags may + * be used. + */ + String remediation(); + + /** + * Typical issue severity level. + * + * @return The typical {@link AuditIssueSeverity} level. + */ + AuditIssueSeverity typicalSeverity(); + + /** + * This method returns an index of the issue type. See the Burp Scanner + * documentation for a listing of all the issue types. + * + * @return An index of the issue type. + */ + int typeIndex(); + + /** + * This method can be used to create a default implementation of an audit + * issue definition. + * + * @param name The name of the issue type. + * @param background The background description for the type of issue. + * @param remediation The background description of the remediation for + * this type of issue. + * @param typicalSeverity The typical {@link AuditIssueSeverity} level. + * + * @return The audit issue definition. + */ + static AuditIssueDefinition auditIssueDefinition(String name, String background, String remediation, AuditIssueSeverity typicalSeverity) + { + return FACTORY.auditIssueDefinition(name, background, remediation, typicalSeverity); + } +} diff --git a/src/burp/api/montoya/scanner/audit/issues/AuditIssueSeverity.java b/src/burp/api/montoya/scanner/audit/issues/AuditIssueSeverity.java new file mode 100644 index 0000000..2940cd8 --- /dev/null +++ b/src/burp/api/montoya/scanner/audit/issues/AuditIssueSeverity.java @@ -0,0 +1,21 @@ +/* + * Copyright (c) 2022-2023. PortSwigger Ltd. All rights reserved. + * + * This code may be used to extend the functionality of Burp Suite Community Edition + * and Burp Suite Professional, provided that this usage does not violate the + * license terms for those products. + */ + +package burp.api.montoya.scanner.audit.issues; + +/** + * This enum represents the severity level of an audit issue. + */ +public enum AuditIssueSeverity +{ + HIGH, + MEDIUM, + LOW, + INFORMATION, + FALSE_POSITIVE +} diff --git a/src/burp/api/montoya/scanner/bchecks/BCheckImportResult.java b/src/burp/api/montoya/scanner/bchecks/BCheckImportResult.java new file mode 100644 index 0000000..c65c364 --- /dev/null +++ b/src/burp/api/montoya/scanner/bchecks/BCheckImportResult.java @@ -0,0 +1,30 @@ +package burp.api.montoya.scanner.bchecks; + +import java.util.List; + +/** + * The result of importing a BCheck + */ +public interface BCheckImportResult +{ + /** + * The status of an imported BCheck + */ + enum Status + { + LOADED_WITHOUT_ERRORS, + LOADED_WITH_ERRORS + } + + /** + * The status of the BCheck after import + * + * @return the status + */ + Status status(); + + /** + * @return a list of errors if the script was invalid or empty is the script was valid. + */ + List importErrors(); +} diff --git a/src/burp/api/montoya/scanner/bchecks/BChecks.java b/src/burp/api/montoya/scanner/bchecks/BChecks.java new file mode 100644 index 0000000..e04b95f --- /dev/null +++ b/src/burp/api/montoya/scanner/bchecks/BChecks.java @@ -0,0 +1,27 @@ +package burp.api.montoya.scanner.bchecks; + +/** + * Provides access to functionality related to BChecks. + */ +public interface BChecks +{ + /** + * This method can be used to import a BCheck. By default, these will be enabled if the + * script imports without errors. + * + * @param script the BCheck script to import + * + * @return The {@link BCheckImportResult} which contains the result of importing the BCheck. + */ + BCheckImportResult importBCheck(String script); + + /** + * This method can be used to import a BCheck. + * + * @param script the BCheck script to import + * @param enabled whether the script should be enabled after successful import + * + * @return The {@link BCheckImportResult} which contains the result of importing the BCheck. + */ + BCheckImportResult importBCheck(String script, boolean enabled); +} diff --git a/src/burp/api/montoya/scope/Scope.java b/src/burp/api/montoya/scope/Scope.java new file mode 100644 index 0000000..2615f8b --- /dev/null +++ b/src/burp/api/montoya/scope/Scope.java @@ -0,0 +1,56 @@ +/* + * Copyright (c) 2022-2023. PortSwigger Ltd. All rights reserved. + * + * This code may be used to extend the functionality of Burp Suite Community Edition + * and Burp Suite Professional, provided that this usage does not violate the + * license terms for those products. + */ + +package burp.api.montoya.scope; + +import burp.api.montoya.core.Registration; + +/** + * Provides access to the functionality related to Burp's + * Suite-wide target scope. + */ +public interface Scope +{ + /** + * This method can be used to query whether a specified URL is within the + * current Suite-wide target scope. + * + * @param url The URL to query. + * + * @return Returns {@code true} if the URL is within the current Suite-wide + * target scope. + */ + boolean isInScope(String url); + + /** + * This method can be used to include the specified URL in the Suite-wide + * target scope. + * + * @param url The URL to include in the Suite-wide target scope. + */ + void includeInScope(String url); + + /** + * This method can be used to exclude the specified URL from the Suite-wide + * target scope. + * + * @param url The URL to exclude from the Suite-wide target scope. + */ + void excludeFromScope(String url); + + /** + * Register a handler which will be notified of + * changes to Burp's Suite-wide target scope. + * + * @param handler An object created by the extension that implements the + * {@link ScopeChangeHandler} interface. + * + * @return The {@link Registration} for the handler. + */ + Registration registerScopeChangeHandler(ScopeChangeHandler handler); +} diff --git a/src/burp/api/montoya/scope/ScopeChange.java b/src/burp/api/montoya/scope/ScopeChange.java new file mode 100644 index 0000000..97d10c4 --- /dev/null +++ b/src/burp/api/montoya/scope/ScopeChange.java @@ -0,0 +1,16 @@ +/* + * Copyright (c) 2022-2023. PortSwigger Ltd. All rights reserved. + * + * This code may be used to extend the functionality of Burp Suite Community Edition + * and Burp Suite Professional, provided that this usage does not violate the + * license terms for those products. + */ + +package burp.api.montoya.scope; + +/** + * Change to Burp's Suite-wide target scope. + */ +public interface ScopeChange +{ +} diff --git a/src/burp/api/montoya/scope/ScopeChangeHandler.java b/src/burp/api/montoya/scope/ScopeChangeHandler.java new file mode 100644 index 0000000..ef21f34 --- /dev/null +++ b/src/burp/api/montoya/scope/ScopeChangeHandler.java @@ -0,0 +1,27 @@ +/* + * Copyright (c) 2022-2023. PortSwigger Ltd. All rights reserved. + * + * This code may be used to extend the functionality of Burp Suite Community Edition + * and Burp Suite Professional, provided that this usage does not violate the + * license terms for those products. + */ + +package burp.api.montoya.scope; + +/** + * Extensions can implement this interface and then call + * {@link Scope#registerScopeChangeHandler(ScopeChangeHandler)} to register a scope change + * handler. The handler will be notified whenever a change occurs to Burp's + * Suite-wide target scope. + */ +public interface ScopeChangeHandler +{ + /** + * This method is invoked whenever a change occurs to Burp's Suite-wide + * target scope. + * + * @param scopeChange An object representing the change to Burp's + * Suite-wide target scope. + */ + void scopeChanged(ScopeChange scopeChange); +} diff --git a/src/burp/api/montoya/sitemap/SiteMap.java b/src/burp/api/montoya/sitemap/SiteMap.java new file mode 100644 index 0000000..6f81fc5 --- /dev/null +++ b/src/burp/api/montoya/sitemap/SiteMap.java @@ -0,0 +1,78 @@ +/* + * Copyright (c) 2022-2023. PortSwigger Ltd. All rights reserved. + * + * This code may be used to extend the functionality of Burp Suite Community Edition + * and Burp Suite Professional, provided that this usage does not violate the + * license terms for those products. + */ + +package burp.api.montoya.sitemap; + +import burp.api.montoya.http.message.HttpRequestResponse; +import burp.api.montoya.scanner.ScanCheck; +import burp.api.montoya.scanner.audit.issues.AuditIssue; + +import java.util.List; + +/** + * Provides methods for querying and modifying Burp's site map. + */ +public interface SiteMap +{ + /** + * This method filters out the site map according to the passed {@link SiteMapFilter} + * object and returns a list of matched {@link HttpRequestResponse} items. + * + * @param filter This parameter can be used to specify a filter, in order to extract a + * specific subset of the site map. + * + * @return A list of filtered items from the site map. + */ + List requestResponses(SiteMapFilter filter); + + /** + * This method returns details of all items in the site map. + * + * @return A list of all items from the site map. + */ + List requestResponses(); + + /** + * This method returns current audit issues for URLs in the site map that are matched by the + * {@link SiteMapFilter} object. + * + * @param filter This parameter can be used to specify a filter, in order to extract issues + * for a specific subset of the site map. + * + * @return A filtered list of audit issues. + */ + List issues(SiteMapFilter filter); + + /** + * This method returns all the current audit issues for URLs in the site map. + * + * @return A list of audit issues. + */ + List issues(); + + /** + * This method can be used to add an {@link HttpRequestResponse} item to Burp's site + * map with the specified request/response details. This will overwrite the details of any + * existing matching item in the site map. + * + * @param requestResponse Item to be added to the site map + */ + void add(HttpRequestResponse requestResponse); + + /** + * Register a new Audit issue. Note: Wherever possible, extensions + * should implement custom Scanner checks using {@link ScanCheck} and report issues + * via those checks, to integrate with Burp's user-driven workflow, and ensure proper + * consolidation of duplicate reported issues. This method is only designed for tasks + * outside the normal testing workflow, such as porting results from other scanning tools. + * + * @param auditIssue An object created by the extension that implements the + * {@link AuditIssue} interface. + */ + void add(AuditIssue auditIssue); +} diff --git a/src/burp/api/montoya/sitemap/SiteMapFilter.java b/src/burp/api/montoya/sitemap/SiteMapFilter.java new file mode 100644 index 0000000..b1f7d33 --- /dev/null +++ b/src/burp/api/montoya/sitemap/SiteMapFilter.java @@ -0,0 +1,40 @@ +/* + * Copyright (c) 2022-2023. PortSwigger Ltd. All rights reserved. + * + * This code may be used to extend the functionality of Burp Suite Community Edition + * and Burp Suite Professional, provided that this usage does not violate the + * license terms for those products. + */ + +package burp.api.montoya.sitemap; + +import static burp.api.montoya.internal.ObjectFactoryLocator.FACTORY; + +/** + * This interface is used to filter items when querying Burp's site map. + */ +public interface SiteMapFilter +{ + /** + * Invoked by Burp to check whether a given site map node matches the filter. + * + * @param node Site map node to match. + * + * @return Returns true if the site map node matches the filter. + */ + boolean matches(SiteMapNode node); + + /** + * This method returns a site map filter object that matches site map nodes with URLs + * starting with the specified prefix. Note that the prefix is case-sensitive. + * + * @param prefix Case-sensitive URL prefix used to match site tree nodes. If {@code null} is + * passed, the resulting filter will match all site map nodes. + * + * @return A site map filter object that matches nodes via a URL prefix + */ + static SiteMapFilter prefixFilter(String prefix) + { + return FACTORY.prefixFilter(prefix); + } +} diff --git a/src/burp/api/montoya/sitemap/SiteMapNode.java b/src/burp/api/montoya/sitemap/SiteMapNode.java new file mode 100644 index 0000000..a411c33 --- /dev/null +++ b/src/burp/api/montoya/sitemap/SiteMapNode.java @@ -0,0 +1,22 @@ +/* + * Copyright (c) 2022-2023. PortSwigger Ltd. All rights reserved. + * + * This code may be used to extend the functionality of Burp Suite Community Edition + * and Burp Suite Professional, provided that this usage does not violate the + * license terms for those products. + */ + +package burp.api.montoya.sitemap; + +/** + * This interface is used to represent items in the Burp's site map. + */ +public interface SiteMapNode +{ + /** + * Retrieve the URL associated with the site map's node. + * + * @return The URL of the node. + */ + String url(); +} diff --git a/src/burp/api/montoya/ui/Selection.java b/src/burp/api/montoya/ui/Selection.java new file mode 100644 index 0000000..576f95b --- /dev/null +++ b/src/burp/api/montoya/ui/Selection.java @@ -0,0 +1,67 @@ +/* + * Copyright (c) 2022-2023. PortSwigger Ltd. All rights reserved. + * + * This code may be used to extend the functionality of Burp Suite Community Edition + * and Burp Suite Professional, provided that this usage does not violate the + * license terms for those products. + */ + +package burp.api.montoya.ui; + +import burp.api.montoya.core.ByteArray; +import burp.api.montoya.core.Range; + +import static burp.api.montoya.internal.ObjectFactoryLocator.FACTORY; + +/** + * Provides helpful information and functionality relating to a user's selection within the user interface. + */ +public interface Selection +{ + /** + * @return The contents that are derived from within the user's selection range. + */ + ByteArray contents(); + + /** + * @return The positional data of where the user has selected. + */ + Range offsets(); + + /** + * @param selectionContents The contents of the selection. + * + * @return A new instance of {@link Selection} + */ + static Selection selection(ByteArray selectionContents) + { + return FACTORY.selection(selectionContents); + } + + /** + * Create an instance of {@link Selection} without content data. + * + * @param startIndexInclusive The start position of the selection range. + * @param endIndexExclusive The end position of the selection range. + * + * @return A new instance of {@link Selection} + */ + static Selection selection(int startIndexInclusive, int endIndexExclusive) + { + return FACTORY.selection(startIndexInclusive, endIndexExclusive); + } + + /** + * Create an instance of {@link Selection}. + * + * @param selectionContents The contents of the selection. + * @param startIndexInclusive The start position of the selection range. + * @param endIndexExclusive The end position of the selection range. + * + * @return A new instance of {@link Selection} + */ + static Selection selection(ByteArray selectionContents, int startIndexInclusive, int endIndexExclusive) + { + return FACTORY.selection(selectionContents, startIndexInclusive, endIndexExclusive); + } +} diff --git a/src/burp/api/montoya/ui/Theme.java b/src/burp/api/montoya/ui/Theme.java new file mode 100644 index 0000000..7e40ccb --- /dev/null +++ b/src/burp/api/montoya/ui/Theme.java @@ -0,0 +1,18 @@ +/* + * Copyright (c) 2022-2023. PortSwigger Ltd. All rights reserved. + * + * This code may be used to extend the functionality of Burp Suite Community Edition + * and Burp Suite Professional, provided that this usage does not violate the + * license terms for those products. + */ + +package burp.api.montoya.ui; + +/** + * This enum contains the different themes available in Burp Suites user interface. + */ +public enum Theme +{ + DARK, + LIGHT +} diff --git a/src/burp/api/montoya/ui/UserInterface.java b/src/burp/api/montoya/ui/UserInterface.java new file mode 100644 index 0000000..cfa8894 --- /dev/null +++ b/src/burp/api/montoya/ui/UserInterface.java @@ -0,0 +1,154 @@ +/* + * Copyright (c) 2022-2023. PortSwigger Ltd. All rights reserved. + * + * This code may be used to extend the functionality of Burp Suite Community Edition + * and Burp Suite Professional, provided that this usage does not violate the + * license terms for those products. + */ + +package burp.api.montoya.ui; + +import burp.api.montoya.core.Registration; +import burp.api.montoya.ui.contextmenu.ContextMenuItemsProvider; +import burp.api.montoya.ui.editor.EditorOptions; +import burp.api.montoya.ui.editor.HttpRequestEditor; +import burp.api.montoya.ui.editor.HttpResponseEditor; +import burp.api.montoya.ui.editor.RawEditor; +import burp.api.montoya.ui.editor.WebSocketMessageEditor; +import burp.api.montoya.ui.editor.extension.HttpRequestEditorProvider; +import burp.api.montoya.ui.editor.extension.HttpResponseEditorProvider; +import burp.api.montoya.ui.editor.extension.WebSocketMessageEditorProvider; +import burp.api.montoya.ui.menu.MenuBar; +import burp.api.montoya.ui.swing.SwingUtils; + +import java.awt.Component; +import java.awt.Font; + +/** + * This interface gives you access to various user interface related features. + * Such as registering your own User Interface providers, creating instances of Burps various editors + * and applying themes to custom components. + */ +public interface UserInterface +{ + /** + * @return The Burp Suite {@link MenuBar}. + */ + MenuBar menuBar(); + + /** + * Add a custom tab to the main Burp Suite window. + * + * @param title The text to be displayed in the tab heading. + * @param component The component that will be rendered within the custom tab. + * + * @return A {@link Registration} of the custom suite tab. + */ + Registration registerSuiteTab(String title, Component component); + + /** + * This method can be used to register a provider of custom context menu items. + * + * @param provider The provider to register. + * + * @return A {@link Registration} of the context menu item provider. + */ + Registration registerContextMenuItemsProvider(ContextMenuItemsProvider provider); + + /** + * This method can be used to register a provider of custom HTTP request editors. + * + * @param provider The provider to register. + * + * @return A {@link Registration} of the HTTP request editor provider. + */ + Registration registerHttpRequestEditorProvider(HttpRequestEditorProvider provider); + + /** + * This method can be used to register a provider of custom HTTP response editors. + * + * @param provider The provider to register. + * + * @return A {@link Registration} of the HTTP response editor provider. + */ + Registration registerHttpResponseEditorProvider(HttpResponseEditorProvider provider); + + /** + * This method can be used to register a provider of custom Web Socket message editors. + * + * @param provider The provider to register. + * + * @return A {@link Registration} of the Web Socket message editor provider. + */ + Registration registerWebSocketMessageEditorProvider(WebSocketMessageEditorProvider provider); + + /** + * Create a new instance of Burp's plain text editor, for the extension to use in its own UI. + * + * @param options Optional options to apply to the editor. + * + * @return An instance of the {@link RawEditor} interface. + */ + RawEditor createRawEditor(EditorOptions... options); + + /** + * Create a new instance of Burp's WebSocket message editor, for the extension to use in its own UI. + * + * @param options Optional options to apply to the editor. + * + * @return An instance of the {@link WebSocketMessageEditor} interface. + */ + WebSocketMessageEditor createWebSocketMessageEditor(EditorOptions... options); + + /** + * Create a new instance of Burp's HTTP request editor, for the extension to use in its own UI. + * + * @param options Optional options to apply to the editor. + * + * @return An instance of the {@link HttpRequestEditor} interface. + */ + HttpRequestEditor createHttpRequestEditor(EditorOptions... options); + + /** + * Create a new instance of Burp's HTTP response editor, for the extension to use in its own UI. + * + * @param options Optional options to apply to the editor. + * + * @return An instance of the {@link HttpResponseEditor} interface. + */ + HttpResponseEditor createHttpResponseEditor(EditorOptions... options); + + /** + * Customize UI components in line with Burp's UI style, including font size, colors, table line spacing, etc. + * The action is performed recursively on any child components of the passed-in component. + * + * @param component The component to be customized. + */ + void applyThemeToComponent(Component component); + + /** + * Identify the theme currently being used. + * + * @return The current {@link Theme} + */ + Theme currentTheme(); + + /** + * Access the message editor's font type and size. + * + * @return The current {@link java.awt.Font}, as specified in the Settings dialog under the HTTP message display setting. + */ + Font currentEditorFont(); + + /** + * Access Burp's font size. + * + * @return The current {@link java.awt.Font}, as specified in the Settings dialog under the Appearance setting. + */ + Font currentDisplayFont(); + + /** + * @return An instance of {@link SwingUtils} + */ + SwingUtils swingUtils(); +} diff --git a/src/burp/api/montoya/ui/contextmenu/AuditIssueContextMenuEvent.java b/src/burp/api/montoya/ui/contextmenu/AuditIssueContextMenuEvent.java new file mode 100644 index 0000000..eda9841 --- /dev/null +++ b/src/burp/api/montoya/ui/contextmenu/AuditIssueContextMenuEvent.java @@ -0,0 +1,25 @@ +/* + * Copyright (c) 2023. PortSwigger Ltd. All rights reserved. + * + * This code may be used to extend the functionality of Burp Suite Community Edition + * and Burp Suite Professional, provided that this usage does not violate the + * license terms for those products. + */ + +package burp.api.montoya.ui.contextmenu; + +import burp.api.montoya.core.ToolSource; +import burp.api.montoya.scanner.audit.issues.AuditIssue; + +import java.util.List; + +public interface AuditIssueContextMenuEvent extends ComponentEvent, ToolSource, InvocationSource +{ + /** + * This method can be used to retrieve details of the Scanner audit issues that were selected by the user when the context menu was invoked. + * This will return an empty list if no issues are applicable to the invocation. + * + * @return a List of {@link AuditIssue} objects representing the items that were shown or selected by the user when the context menu was invoked. + */ + List selectedIssues(); +} diff --git a/src/burp/api/montoya/ui/contextmenu/ComponentEvent.java b/src/burp/api/montoya/ui/contextmenu/ComponentEvent.java new file mode 100644 index 0000000..59cefd1 --- /dev/null +++ b/src/burp/api/montoya/ui/contextmenu/ComponentEvent.java @@ -0,0 +1,25 @@ +/* + * Copyright (c) 2022-2023. PortSwigger Ltd. All rights reserved. + * + * This code may be used to extend the functionality of Burp Suite Community Edition + * and Burp Suite Professional, provided that this usage does not violate the + * license terms for those products. + */ + +package burp.api.montoya.ui.contextmenu; + +import java.awt.event.InputEvent; + +/** + * This interface describes an action or event that has occurred with a user interface component. + */ +public interface ComponentEvent +{ + /** + * This method can be used to retrieve the native Java input event that was + * the trigger for the context menu invocation. + * + * @return The {@link InputEvent} that was the trigger for the context menu invocation. + */ + InputEvent inputEvent(); +} diff --git a/src/burp/api/montoya/ui/contextmenu/ContextMenuEvent.java b/src/burp/api/montoya/ui/contextmenu/ContextMenuEvent.java new file mode 100644 index 0000000..10b448f --- /dev/null +++ b/src/burp/api/montoya/ui/contextmenu/ContextMenuEvent.java @@ -0,0 +1,47 @@ +/* + * Copyright (c) 2022-2023. PortSwigger Ltd. All rights reserved. + * + * This code may be used to extend the functionality of Burp Suite Community Edition + * and Burp Suite Professional, provided that this usage does not violate the + * license terms for those products. + */ + +package burp.api.montoya.ui.contextmenu; + +import burp.api.montoya.core.ToolSource; +import burp.api.montoya.http.message.HttpRequestResponse; +import burp.api.montoya.scanner.audit.issues.AuditIssue; + +import java.util.List; +import java.util.Optional; + +/** + * Provides useful information when generating context menu items from a {@link ContextMenuItemsProvider}. + */ +public interface ContextMenuEvent extends ComponentEvent, ToolSource, InvocationSource +{ + /** + * This method can be used to retrieve details of the currently selected HTTP request/response when the context menu was invoked. + * + * @return an {@link Optional} describing the currently selected request response with selection metadata. + */ + Optional messageEditorRequestResponse(); + + /** + * This method can be used to retrieve details of the currently selected HTTP request/response pair that was + * selected by the user when the context menu was invoked. This will return an empty list if the user has not made a selection. + * + * @return A list of request responses that have been selected by the user. + */ + List selectedRequestResponses(); + + /** + * This method can be used to retrieve details of the Scanner issues that were selected by the user when the context menu was invoked. + * This will return an empty list if no issues are applicable to the invocation. + * + * @return a List of {@link AuditIssue} objects representing the items that were shown or selected by the user when the context menu was invoked. + * @deprecated Use {@link ContextMenuItemsProvider#provideMenuItems(AuditIssueContextMenuEvent)} instead. + */ + @Deprecated + List selectedIssues(); +} diff --git a/src/burp/api/montoya/ui/contextmenu/ContextMenuItemsProvider.java b/src/burp/api/montoya/ui/contextmenu/ContextMenuItemsProvider.java new file mode 100644 index 0000000..8842751 --- /dev/null +++ b/src/burp/api/montoya/ui/contextmenu/ContextMenuItemsProvider.java @@ -0,0 +1,59 @@ +/* + * Copyright (c) 2022-2023. PortSwigger Ltd. All rights reserved. + * + * This code may be used to extend the functionality of Burp Suite Community Edition + * and Burp Suite Professional, provided that this usage does not violate the + * license terms for those products. + */ + +package burp.api.montoya.ui.contextmenu; + +import java.awt.Component; +import java.util.List; + +import static java.util.Collections.emptyList; + +/** + * This interface allows extensions to implement and register a provider for custom context menu items. + */ +public interface ContextMenuItemsProvider +{ + /** + * Invoked by Burp Suite when the user requests a context menu with HTTP request/response information in the user interface. + * Extensions should return {@code null} or {@link java.util.Collections#emptyList()} from this method, to indicate that no menu items are required. + * + * @param event This object can be queried to find out about HTTP request/responses that are associated with the context menu invocation. + * + * @return A list of custom menu items (which may include sub-menus, checkbox menu items, etc.) that should be displayed. + */ + default List provideMenuItems(ContextMenuEvent event) + { + return emptyList(); + } + + /** + * Invoked by Burp Suite when the user requests a context menu with WebSocket information in the user interface. + * Extensions should return {@code null} or {@link java.util.Collections#emptyList()} from this method, to indicate that no menu items are required. + * + * @param event This object can be queried to find out about WebSocket messages that are associated with the context menu invocation. + * + * @return A list of custom menu items (which may include sub-menus, checkbox menu items, etc.) that should be displayed. + */ + default List provideMenuItems(WebSocketContextMenuEvent event) + { + return emptyList(); + } + + /** + * Invoked by Burp Suite when the user requests a context menu with audit issue information in the user interface. + * Extensions should return {@code null} or {@link java.util.Collections#emptyList()} from this method, to indicate that no menu items are required. + * + * @param event This object can be queried to find out about audit issues that are associated with the context menu invocation. + * + * @return A list of custom menu items (which may include sub-menus, checkbox menu items, etc.) that should be displayed. + */ + default List provideMenuItems(AuditIssueContextMenuEvent event) + { + return emptyList(); + } +} diff --git a/src/burp/api/montoya/ui/contextmenu/InvocationSource.java b/src/burp/api/montoya/ui/contextmenu/InvocationSource.java new file mode 100644 index 0000000..1e233ab --- /dev/null +++ b/src/burp/api/montoya/ui/contextmenu/InvocationSource.java @@ -0,0 +1,29 @@ +/* + * Copyright (c) 2022-2023. PortSwigger Ltd. All rights reserved. + * + * This code may be used to extend the functionality of Burp Suite Community Edition + * and Burp Suite Professional, provided that this usage does not violate the + * license terms for those products. + */ + +package burp.api.montoya.ui.contextmenu; + +/** + * Provides information about the source from which a context menu was invoked. + */ +public interface InvocationSource +{ + /** + * @return An instance of {@link InvocationType} which provides the current location of the context menu being invoked. + */ + InvocationType invocationType(); + + /** + * A helper method to allow the extension to ask if the context is within a set of locations. + * + * @param invocationType One or more instances of {@link InvocationType} to check. + * + * @return True if the context menu is being invoked from one of the types that is being checked. + */ + boolean isFrom(InvocationType... invocationType); +} diff --git a/src/burp/api/montoya/ui/contextmenu/InvocationType.java b/src/burp/api/montoya/ui/contextmenu/InvocationType.java new file mode 100644 index 0000000..8aaa19a --- /dev/null +++ b/src/burp/api/montoya/ui/contextmenu/InvocationType.java @@ -0,0 +1,71 @@ +/* + * Copyright (c) 2022-2023. PortSwigger Ltd. All rights reserved. + * + * This code may be used to extend the functionality of Burp Suite Community Edition + * and Burp Suite Professional, provided that this usage does not violate the + * license terms for those products. + */ + +package burp.api.montoya.ui.contextmenu; + +/** + * An enum containing different types of context menu invocations. + */ +public enum InvocationType +{ + MESSAGE_EDITOR_REQUEST, + MESSAGE_EDITOR_RESPONSE, + MESSAGE_VIEWER_REQUEST, + MESSAGE_VIEWER_RESPONSE, + SITE_MAP_TREE, + SITE_MAP_TABLE, + PROXY_HISTORY, + SCANNER_RESULTS, + INTRUDER_PAYLOAD_POSITIONS, + INTRUDER_ATTACK_RESULTS, + SEARCH_RESULTS; + + /** + * @return A helper method to ask if this type contains HTTP messages. + */ + public boolean containsHttpMessage() + { + switch (this) + { + case MESSAGE_EDITOR_REQUEST: + case MESSAGE_EDITOR_RESPONSE: + case MESSAGE_VIEWER_REQUEST: + case MESSAGE_VIEWER_RESPONSE: + case INTRUDER_PAYLOAD_POSITIONS: + return true; + } + + return false; + } + + /** + * @return A helper method to ask if this type contains HTTP request/responses. + */ + public boolean containsHttpRequestResponses() + { + switch (this) + { + case SITE_MAP_TREE: + case SITE_MAP_TABLE: + case PROXY_HISTORY: + case INTRUDER_ATTACK_RESULTS: + case SEARCH_RESULTS: + return true; + } + + return false; + } + + /** + * @return A helper method to ask if this type contains any scan issues. + */ + public boolean containsScanIssues() + { + return this == SCANNER_RESULTS; + } +} diff --git a/src/burp/api/montoya/ui/contextmenu/MessageEditorHttpRequestResponse.java b/src/burp/api/montoya/ui/contextmenu/MessageEditorHttpRequestResponse.java new file mode 100644 index 0000000..a2abc35 --- /dev/null +++ b/src/burp/api/montoya/ui/contextmenu/MessageEditorHttpRequestResponse.java @@ -0,0 +1,65 @@ +/* + * Copyright (c) 2022-2023. PortSwigger Ltd. All rights reserved. + * + * This code may be used to extend the functionality of Burp Suite Community Edition + * and Burp Suite Professional, provided that this usage does not violate the + * license terms for those products. + */ + +package burp.api.montoya.ui.contextmenu; + +import burp.api.montoya.core.Range; +import burp.api.montoya.http.message.HttpRequestResponse; +import burp.api.montoya.http.message.requests.HttpRequest; +import burp.api.montoya.http.message.responses.HttpResponse; + +import java.util.Optional; + +/** + * This class contains information about a user selection of a request or response within a Burp Suite message editor. + */ +public interface MessageEditorHttpRequestResponse +{ + /** + * @return An {@link SelectionContext} which indicates what data has been selected by the user and has focus. + */ + SelectionContext selectionContext(); + + /** + * This will return {@link Optional#empty()} if the user has not made a selection. + * + * @return An {@link Optional} range of indices that indicates the position of the users current selection. + */ + Optional selectionOffsets(); + + /** + * @return The index of the position for the carat within the current message editor. + */ + int caretPosition(); + + /** + * @return An instance of {@link HttpRequestResponse} which contains the information about the currently displayed or selected HTTP request/response. + */ + HttpRequestResponse requestResponse(); + + /** + * Update the message editor with the HTTP request + * + * @param request the request to update the editor. + */ + void setRequest(HttpRequest request); + + /** + * Update the message editor with the HTTP response + * + * @param response the response to update the editor. + */ + void setResponse(HttpResponse response); + + + enum SelectionContext + { + REQUEST, + RESPONSE + } +} diff --git a/src/burp/api/montoya/ui/contextmenu/WebSocketContextMenuEvent.java b/src/burp/api/montoya/ui/contextmenu/WebSocketContextMenuEvent.java new file mode 100644 index 0000000..15215d5 --- /dev/null +++ b/src/burp/api/montoya/ui/contextmenu/WebSocketContextMenuEvent.java @@ -0,0 +1,32 @@ +/* + * Copyright (c) 2023. PortSwigger Ltd. All rights reserved. + * + * This code may be used to extend the functionality of Burp Suite Community Edition + * and Burp Suite Professional, provided that this usage does not violate the + * license terms for those products. + */ + +package burp.api.montoya.ui.contextmenu; + +import burp.api.montoya.core.ToolSource; + +import java.util.List; +import java.util.Optional; + +public interface WebSocketContextMenuEvent extends ComponentEvent, ToolSource +{ + /** + * This method can be used to retrieve details of the currently selected WebSocket message when the context menu was invoked from an editor. + * + * @return an {@link Optional} describing the currently selected WebSocket message with selection metadata. + */ + Optional messageEditorWebSocket(); + + /** + * This method can be used to retrieve details of the currently selected WebSocket messages that are + * selected by the user when the context menu was invoked. This will return an empty list if the user has not made a selection. + * + * @return A list of WebSocket messages that have been selected by the user. + */ + List selectedWebSocketMessages(); +} diff --git a/src/burp/api/montoya/ui/contextmenu/WebSocketEditorEvent.java b/src/burp/api/montoya/ui/contextmenu/WebSocketEditorEvent.java new file mode 100644 index 0000000..11eacab --- /dev/null +++ b/src/burp/api/montoya/ui/contextmenu/WebSocketEditorEvent.java @@ -0,0 +1,53 @@ +/* + * Copyright (c) 2023. PortSwigger Ltd. All rights reserved. + * + * This code may be used to extend the functionality of Burp Suite Community Edition + * and Burp Suite Professional, provided that this usage does not violate the + * license terms for those products. + */ + +package burp.api.montoya.ui.contextmenu; + +import burp.api.montoya.core.ByteArray; +import burp.api.montoya.core.Range; +import burp.api.montoya.core.ToolSource; + +import java.util.Optional; + +public interface WebSocketEditorEvent extends ComponentEvent, ToolSource +{ + /** + * @return The contents of the message editor. + */ + ByteArray getContents(); + + /** + * This method can be used to set the content within the message editor programmatically. + * If the editor is read only the contents will not be updated. + * + * @param contents The content to set in the message editor. + */ + void setContents(ByteArray contents); + + /** + * @return the WebSocket message used to populate the editor. + */ + WebSocketMessage webSocketMessage(); + + /** + * @return if the editor is read only. + */ + boolean isReadOnly(); + + /** + * This will return {@link Optional#empty()} if the user has not made a selection. + * + * @return An {@link Optional} range of indices that indicates the position of the users current selection. + */ + Optional selectionOffsets(); + + /** + * @return The index of the position for the carat within the current message editor. + */ + int caretPosition(); +} diff --git a/src/burp/api/montoya/ui/contextmenu/WebSocketMessage.java b/src/burp/api/montoya/ui/contextmenu/WebSocketMessage.java new file mode 100644 index 0000000..b398ae9 --- /dev/null +++ b/src/burp/api/montoya/ui/contextmenu/WebSocketMessage.java @@ -0,0 +1,39 @@ +/* + * Copyright (c) 2023. PortSwigger Ltd. All rights reserved. + * + * This code may be used to extend the functionality of Burp Suite Community Edition + * and Burp Suite Professional, provided that this usage does not violate the + * license terms for those products. + */ + +package burp.api.montoya.ui.contextmenu; + +import burp.api.montoya.core.Annotations; +import burp.api.montoya.core.ByteArray; +import burp.api.montoya.http.message.requests.HttpRequest; +import burp.api.montoya.websocket.Direction; + +public interface WebSocketMessage +{ + /** + * This method retrieves the annotations for the message. + * + * @return The {@link Annotations} for the message. + */ + Annotations annotations(); + + /** + * @return The direction of the message. + */ + Direction direction(); + + /** + * @return WebSocket payload. + */ + ByteArray payload(); + + /** + * @return The {@link HttpRequest} used to create the WebSocket. + */ + HttpRequest upgradeRequest(); +} diff --git a/src/burp/api/montoya/ui/editor/Editor.java b/src/burp/api/montoya/ui/editor/Editor.java new file mode 100644 index 0000000..8e68b58 --- /dev/null +++ b/src/burp/api/montoya/ui/editor/Editor.java @@ -0,0 +1,49 @@ +/* + * Copyright (c) 2022-2023. PortSwigger Ltd. All rights reserved. + * + * This code may be used to extend the functionality of Burp Suite Community Edition + * and Burp Suite Professional, provided that this usage does not violate the + * license terms for those products. + */ + +package burp.api.montoya.ui.editor; + +import burp.api.montoya.ui.Selection; + +import java.awt.Component; +import java.util.Optional; + +/** + * Provides the shared behaviour between the different editor types. + */ +public interface Editor +{ + /** + * Update the search expression that is shown in the search bar below the editor. + * + * @param expression The search expression. + */ + void setSearchExpression(String expression); + + /** + * @return True if the user has modified the contents of the editor since the last time the content was set programmatically. + */ + boolean isModified(); + + /** + * @return The index of the position for the carat within the current message editor. + */ + int caretPosition(); + + /** + * This will return {@link Optional#empty()} if the user has not made a selection. + * + * @return An {@link Optional} containing the users current selection in the editor. + */ + Optional selection(); + + /** + * @return UI component of the editor, for extensions to add to their own UI. + */ + Component uiComponent(); +} diff --git a/src/burp/api/montoya/ui/editor/EditorOptions.java b/src/burp/api/montoya/ui/editor/EditorOptions.java new file mode 100644 index 0000000..d104f34 --- /dev/null +++ b/src/burp/api/montoya/ui/editor/EditorOptions.java @@ -0,0 +1,17 @@ +/* + * Copyright (c) 2022-2023. PortSwigger Ltd. All rights reserved. + * + * This code may be used to extend the functionality of Burp Suite Community Edition + * and Burp Suite Professional, provided that this usage does not violate the + * license terms for those products. + */ + +package burp.api.montoya.ui.editor; + +/** + * These options allow you to configure additional behaviour to {@link Editor} implementations. + */ +public enum EditorOptions +{ + READ_ONLY +} diff --git a/src/burp/api/montoya/ui/editor/HttpRequestEditor.java b/src/burp/api/montoya/ui/editor/HttpRequestEditor.java new file mode 100644 index 0000000..0016efc --- /dev/null +++ b/src/burp/api/montoya/ui/editor/HttpRequestEditor.java @@ -0,0 +1,67 @@ +/* + * Copyright (c) 2022-2023. PortSwigger Ltd. All rights reserved. + * + * This code may be used to extend the functionality of Burp Suite Community Edition + * and Burp Suite Professional, provided that this usage does not violate the + * license terms for those products. + */ + +package burp.api.montoya.ui.editor; + +import burp.api.montoya.http.message.requests.HttpRequest; +import burp.api.montoya.ui.Selection; + +import java.awt.Component; +import java.util.Optional; + +/** + * Provides extensions with an instance of Burp Suites HTTP request editor to use in their own user interface. + */ +public interface HttpRequestEditor extends Editor +{ + /** + * @return an instance of {@link HttpRequest} derived from the contents of the editor. + */ + HttpRequest getRequest(); + + /** + * Display the contents of an HTTP request in the editor. + * + * @param request The HTTP request to be set. + */ + void setRequest(HttpRequest request); + + /** + * Update the search expression that is shown in the search bar below the editor. + * + * @param expression The search expression. + */ + @Override + void setSearchExpression(String expression); + + /** + * @return True if the user has modified the contents of the editor since the last time the content was set programmatically. + */ + @Override + boolean isModified(); + + /** + * @return The index of the position for the carat within the current message editor. + */ + @Override + int caretPosition(); + + /** + * This will return {@link Optional#empty()} if the user has not made a selection. + * + * @return An {@link Optional} containing the users current selection in the editor. + */ + @Override + Optional selection(); + + /** + * @return UI component of the editor, for extensions to add to their own UI. + */ + @Override + Component uiComponent(); +} diff --git a/src/burp/api/montoya/ui/editor/HttpResponseEditor.java b/src/burp/api/montoya/ui/editor/HttpResponseEditor.java new file mode 100644 index 0000000..5a98eac --- /dev/null +++ b/src/burp/api/montoya/ui/editor/HttpResponseEditor.java @@ -0,0 +1,67 @@ +/* + * Copyright (c) 2022-2023. PortSwigger Ltd. All rights reserved. + * + * This code may be used to extend the functionality of Burp Suite Community Edition + * and Burp Suite Professional, provided that this usage does not violate the + * license terms for those products. + */ + +package burp.api.montoya.ui.editor; + +import burp.api.montoya.http.message.responses.HttpResponse; +import burp.api.montoya.ui.Selection; + +import java.awt.Component; +import java.util.Optional; + +/** + * Provides extensions with an instance of Burp Suites HTTP response editor to use in their own user interface. + */ +public interface HttpResponseEditor extends Editor +{ + /** + * @return an instance of {@link HttpResponse} derived from the contents of the editor. + */ + HttpResponse getResponse(); + + /** + * Display the contents of an HTTP response in the editor. + * + * @param response The HTTP response to be set. + */ + void setResponse(HttpResponse response); + + /** + * Update the search expression that is shown in the search bar below the editor. + * + * @param expression The search expression. + */ + @Override + void setSearchExpression(String expression); + + /** + * @return True if the user has modified the contents of the editor since the last time the content was set programmatically. + */ + @Override + boolean isModified(); + + /** + * @return The index of the position for the carat within the current message editor. + */ + @Override + int caretPosition(); + + /** + * This will return {@link Optional#empty()} if the user has not made a selection. + * + * @return An {@link Optional} containing the users current selection in the editor. + */ + @Override + Optional selection(); + + /** + * @return UI component of the editor, for extensions to add to their own UI. + */ + @Override + Component uiComponent(); +} diff --git a/src/burp/api/montoya/ui/editor/RawEditor.java b/src/burp/api/montoya/ui/editor/RawEditor.java new file mode 100644 index 0000000..e79b7b7 --- /dev/null +++ b/src/burp/api/montoya/ui/editor/RawEditor.java @@ -0,0 +1,72 @@ +/* + * Copyright (c) 2022-2023. PortSwigger Ltd. All rights reserved. + * + * This code may be used to extend the functionality of Burp Suite Community Edition + * and Burp Suite Professional, provided that this usage does not violate the + * license terms for those products. + */ + +package burp.api.montoya.ui.editor; + +import burp.api.montoya.core.ByteArray; +import burp.api.montoya.ui.Selection; + +import java.awt.Component; +import java.util.Optional; + +/** + * Provides extensions with an instance of Burp Suite's HTTP text editor to use in their own user interface. + */ +public interface RawEditor extends Editor +{ + /** + * @param editable Boolean flag to toggle if this text editor is editable or not. + */ + void setEditable(boolean editable); + + /** + * @return The contents of the text editor. + */ + ByteArray getContents(); + + /** + * This method can be used to set content within the text editor programmatically + * + * @param contents The content to set in the text editor. + */ + void setContents(ByteArray contents); + + /** + * Update the search expression that is shown in the search bar below the editor. + * + * @param expression The search expression. + */ + @Override + void setSearchExpression(String expression); + + /** + * @return True if the user has modified the contents of the editor since the last time the content was set programmatically. + */ + @Override + boolean isModified(); + + /** + * @return The index of the position for the carat within the current message editor. + */ + @Override + int caretPosition(); + + /** + * This will return {@link Optional#empty()} if the user has not made a selection. + * + * @return An {@link Optional} containing the users current selection in the editor. + */ + @Override + Optional selection(); + + /** + * @return UI component of the editor, for extensions to add to their own UI. + */ + @Override + Component uiComponent(); +} diff --git a/src/burp/api/montoya/ui/editor/WebSocketMessageEditor.java b/src/burp/api/montoya/ui/editor/WebSocketMessageEditor.java new file mode 100644 index 0000000..1013c40 --- /dev/null +++ b/src/burp/api/montoya/ui/editor/WebSocketMessageEditor.java @@ -0,0 +1,67 @@ +/* + * Copyright (c) 2023. PortSwigger Ltd. All rights reserved. + * + * This code may be used to extend the functionality of Burp Suite Community Edition + * and Burp Suite Professional, provided that this usage does not violate the + * license terms for those products. + */ + +package burp.api.montoya.ui.editor; + +import burp.api.montoya.core.ByteArray; +import burp.api.montoya.ui.Selection; + +import java.awt.Component; +import java.util.Optional; + +/** + * Provides extensions with an instance of Burp Suite's WebSocket message editor to use in their own user interface. + */ +public interface WebSocketMessageEditor extends Editor +{ + /** + * @return The contents of the message editor. + */ + ByteArray getContents(); + + /** + * This method can be used to set content within the message editor programmatically + * + * @param contents The content to set in the message editor. + */ + void setContents(ByteArray contents); + + /** + * Update the search expression that is shown in the search bar below the editor. + * + * @param expression The search expression. + */ + @Override + void setSearchExpression(String expression); + + /** + * @return True if the user has modified the contents of the editor since the last time the content was set programmatically. + */ + @Override + boolean isModified(); + + /** + * @return The index of the position for the carat within the current message editor. + */ + @Override + int caretPosition(); + + /** + * This will return {@link Optional#empty()} if the user has not made a selection. + * + * @return An {@link Optional} containing the users current selection in the editor. + */ + @Override + Optional selection(); + + /** + * @return UI component of the editor, for extensions to add to their own UI. + */ + @Override + Component uiComponent(); +} diff --git a/src/burp/api/montoya/ui/editor/extension/EditorCreationContext.java b/src/burp/api/montoya/ui/editor/extension/EditorCreationContext.java new file mode 100644 index 0000000..56c5498 --- /dev/null +++ b/src/burp/api/montoya/ui/editor/extension/EditorCreationContext.java @@ -0,0 +1,43 @@ +/* + * Copyright (c) 2022-2023. PortSwigger Ltd. All rights reserved. + * + * This code may be used to extend the functionality of Burp Suite Community Edition + * and Burp Suite Professional, provided that this usage does not violate the + * license terms for those products. + */ + +package burp.api.montoya.ui.editor.extension; + +import burp.api.montoya.core.ToolSource; + +/** + * This interface is used by an + * ExtensionHttpRequestEditor or ExtensionHttpResponseEditor to obtain + * details about the currently displayed message. + * Extensions that create instances of Burp's HTTP message editor can + * optionally provide an implementation of + * IMessageEditorController, which the editor will invoke when it + * requires further information about the current message (for example, to send + * it to another Burp tool). Extensions that provide custom editor tabs via an + * IMessageEditorTabFactory will receive a reference to an + * IMessageEditorController object for each tab instance they + * generate, which the tab can invoke if it requires further information about + * the current message. + */ +public interface EditorCreationContext +{ + /** + * Indicates which Burp tool is requesting the editor. + * + * @return The tool requesting an editor + */ + ToolSource toolSource(); + + /** + * Indicates which modes the Burp tool requests of the editor. + * e.g. Proxy expects a read only editor, Repeater expects the default editor. + * + * @return The mode required by the editor. + */ + EditorMode editorMode(); +} \ No newline at end of file diff --git a/src/burp/api/montoya/ui/editor/extension/EditorMode.java b/src/burp/api/montoya/ui/editor/extension/EditorMode.java new file mode 100644 index 0000000..d84a31d --- /dev/null +++ b/src/burp/api/montoya/ui/editor/extension/EditorMode.java @@ -0,0 +1,18 @@ +/* + * Copyright (c) 2022-2023. PortSwigger Ltd. All rights reserved. + * + * This code may be used to extend the functionality of Burp Suite Community Edition + * and Burp Suite Professional, provided that this usage does not violate the + * license terms for those products. + */ + +package burp.api.montoya.ui.editor.extension; + +/** + * An enum to describe the different modes of Burp Suites message editor. + */ +public enum EditorMode +{ + DEFAULT, + READ_ONLY +} diff --git a/src/burp/api/montoya/ui/editor/extension/ExtensionProvidedEditor.java b/src/burp/api/montoya/ui/editor/extension/ExtensionProvidedEditor.java new file mode 100644 index 0000000..82958d4 --- /dev/null +++ b/src/burp/api/montoya/ui/editor/extension/ExtensionProvidedEditor.java @@ -0,0 +1,58 @@ +/* + * Copyright (c) 2023. PortSwigger Ltd. All rights reserved. + * + * This code may be used to extend the functionality of Burp Suite Community Edition + * and Burp Suite Professional, provided that this usage does not violate the + * license terms for those products. + */ + +package burp.api.montoya.ui.editor.extension; + +import burp.api.montoya.http.message.HttpRequestResponse; +import burp.api.montoya.ui.Selection; + +import java.awt.Component; + +/** + * Provides the shared behaviour between the different extension provided editor types. + */ +public interface ExtensionProvidedEditor +{ + /** + * Sets the provided {@link HttpRequestResponse} object within the editor component. + * + * @param requestResponse The request and response to set in the editor. + */ + void setRequestResponse(HttpRequestResponse requestResponse); + + /** + * A check to determine if the HTTP message editor is enabled for a specific {@link HttpRequestResponse} + * + * @param requestResponse The {@link HttpRequestResponse} to check. + * + * @return True if the HTTP message editor is enabled for the provided request and response. + */ + boolean isEnabledFor(HttpRequestResponse requestResponse); + + /** + * @return The caption located in the message editor tab header. + */ + String caption(); + + /** + * @return The component that is rendered within the message editor tab. + */ + Component uiComponent(); + + /** + * The method should return {@code null} if no data has been selected. + * + * @return The data that is currently selected by the user. + */ + Selection selectedData(); + + /** + * @return True if the user has modified the current message within the editor. + */ + boolean isModified(); +} diff --git a/src/burp/api/montoya/ui/editor/extension/ExtensionProvidedHttpRequestEditor.java b/src/burp/api/montoya/ui/editor/extension/ExtensionProvidedHttpRequestEditor.java new file mode 100644 index 0000000..7c4bff7 --- /dev/null +++ b/src/burp/api/montoya/ui/editor/extension/ExtensionProvidedHttpRequestEditor.java @@ -0,0 +1,71 @@ +/* + * Copyright (c) 2022-2023. PortSwigger Ltd. All rights reserved. + * + * This code may be used to extend the functionality of Burp Suite Community Edition + * and Burp Suite Professional, provided that this usage does not violate the + * license terms for those products. + */ + +package burp.api.montoya.ui.editor.extension; + +import burp.api.montoya.http.message.HttpRequestResponse; +import burp.api.montoya.http.message.requests.HttpRequest; +import burp.api.montoya.ui.Selection; + +import java.awt.Component; + +/** + * Extensions that register an {@link HttpRequestEditorProvider} must return an instance of this interface.
+ * Burp will then use that instance to create custom tabs within its HTTP request editor. + */ +public interface ExtensionProvidedHttpRequestEditor extends ExtensionProvidedEditor +{ + /** + * @return An instance of {@link HttpRequest} derived from the content of the HTTP request editor. + */ + HttpRequest getRequest(); + + /** + * Sets the provided {@link HttpRequestResponse} object within the editor component. + * + * @param requestResponse The request and response to set in the editor. + */ + @Override + void setRequestResponse(HttpRequestResponse requestResponse); + + /** + * A check to determine if the HTTP message editor is enabled for a specific {@link HttpRequestResponse} + * + * @param requestResponse The {@link HttpRequestResponse} to check. + * + * @return True if the HTTP message editor is enabled for the provided request and response. + */ + @Override + boolean isEnabledFor(HttpRequestResponse requestResponse); + + /** + * @return The caption located in the message editor tab header. + */ + @Override + String caption(); + + /** + * @return The component that is rendered within the message editor tab. + */ + @Override + Component uiComponent(); + + /** + * The method should return {@code null} if no data has been selected. + * + * @return The data that is currently selected by the user. + */ + @Override + Selection selectedData(); + + /** + * @return True if the user has modified the current message within the editor. + */ + @Override + boolean isModified(); +} diff --git a/src/burp/api/montoya/ui/editor/extension/ExtensionProvidedHttpResponseEditor.java b/src/burp/api/montoya/ui/editor/extension/ExtensionProvidedHttpResponseEditor.java new file mode 100644 index 0000000..56241bc --- /dev/null +++ b/src/burp/api/montoya/ui/editor/extension/ExtensionProvidedHttpResponseEditor.java @@ -0,0 +1,71 @@ +/* + * Copyright (c) 2022-2023. PortSwigger Ltd. All rights reserved. + * + * This code may be used to extend the functionality of Burp Suite Community Edition + * and Burp Suite Professional, provided that this usage does not violate the + * license terms for those products. + */ + +package burp.api.montoya.ui.editor.extension; + +import burp.api.montoya.http.message.HttpRequestResponse; +import burp.api.montoya.http.message.responses.HttpResponse; +import burp.api.montoya.ui.Selection; + +import java.awt.Component; + +/** + * Extensions that register an {@link HttpResponseEditorProvider} must return an instance of this interface.
+ * Burp will then use that instance to create custom tabs within its HTTP response editor. + */ +public interface ExtensionProvidedHttpResponseEditor extends ExtensionProvidedEditor +{ + /** + * @return An instance of {@link HttpResponse} derived from the content of the HTTP response editor. + */ + HttpResponse getResponse(); + + /** + * Sets the provided {@link HttpRequestResponse} object within the editor component. + * + * @param requestResponse The request and response to set in the editor. + */ + @Override + void setRequestResponse(HttpRequestResponse requestResponse); + + /** + * A check to determine if the HTTP message editor is enabled for a specific {@link HttpRequestResponse} + * + * @param requestResponse The {@link HttpRequestResponse} to check. + * + * @return True if the HTTP message editor is enabled for the provided request and response. + */ + @Override + boolean isEnabledFor(HttpRequestResponse requestResponse); + + /** + * @return The caption located in the message editor tab header. + */ + @Override + String caption(); + + /** + * @return The component that is rendered within the message editor tab. + */ + @Override + Component uiComponent(); + + /** + * The method should return {@code null} if no data has been selected. + * + * @return The data that is currently selected by the user. + */ + @Override + Selection selectedData(); + + /** + * @return True if the user has modified the current message within the editor. + */ + @Override + boolean isModified(); +} diff --git a/src/burp/api/montoya/ui/editor/extension/ExtensionProvidedWebSocketMessageEditor.java b/src/burp/api/montoya/ui/editor/extension/ExtensionProvidedWebSocketMessageEditor.java new file mode 100644 index 0000000..0198e1c --- /dev/null +++ b/src/burp/api/montoya/ui/editor/extension/ExtensionProvidedWebSocketMessageEditor.java @@ -0,0 +1,57 @@ +package burp.api.montoya.ui.editor.extension; + +import burp.api.montoya.core.ByteArray; +import burp.api.montoya.ui.Selection; +import burp.api.montoya.ui.contextmenu.WebSocketMessage; + +import java.awt.Component; + +/** + * Extensions that register an {@link WebSocketMessageEditorProvider} must return an instance of this interface.
+ * Burp will then use that instance to create custom tabs within its Web Socket message editor. + */ +public interface ExtensionProvidedWebSocketMessageEditor +{ + /** + * @return The current message set in the editor as an instance of {@link ByteArray} + */ + ByteArray getMessage(); + + /** + * Sets the provided {@link WebSocketMessage} within the editor component. + * + * @param message The message to set in the editor. + */ + void setMessage(WebSocketMessage message); + + /** + * A check to determine if the Web Socket editor is enabled for a specific {@link WebSocketMessage} message + * + * @param message The {@link WebSocketMessage} to check. + * + * @return True if the Web Socket message editor is enabled for the provided message. + */ + boolean isEnabledFor(WebSocketMessage message); + + /** + * @return The caption located in the message editor tab header. + */ + String caption(); + + /** + * @return The component that is rendered within the message editor tab. + */ + Component uiComponent(); + + /** + * The method should return {@code null} if no data has been selected. + * + * @return The data that is currently selected by the user. + */ + Selection selectedData(); + + /** + * @return True if the user has modified the current message within the editor. + */ + boolean isModified(); +} diff --git a/src/burp/api/montoya/ui/editor/extension/HttpRequestEditorProvider.java b/src/burp/api/montoya/ui/editor/extension/HttpRequestEditorProvider.java new file mode 100644 index 0000000..e9e0b53 --- /dev/null +++ b/src/burp/api/montoya/ui/editor/extension/HttpRequestEditorProvider.java @@ -0,0 +1,24 @@ +/* + * Copyright (c) 2022-2023. PortSwigger Ltd. All rights reserved. + * + * This code may be used to extend the functionality of Burp Suite Community Edition + * and Burp Suite Professional, provided that this usage does not violate the + * license terms for those products. + */ + +package burp.api.montoya.ui.editor.extension; + +/** + * Extensions can register an instance of this interface to provide custom HTTP request editors within Burp's user interface. + */ +public interface HttpRequestEditorProvider +{ + /** + * Invoked by Burp when a new HTTP request editor is required from the extension. + * + * @param creationContext details about the context that is requiring a request editor + * + * @return An instance of {@link ExtensionProvidedHttpRequestEditor} + */ + ExtensionProvidedHttpRequestEditor provideHttpRequestEditor(EditorCreationContext creationContext); +} diff --git a/src/burp/api/montoya/ui/editor/extension/HttpResponseEditorProvider.java b/src/burp/api/montoya/ui/editor/extension/HttpResponseEditorProvider.java new file mode 100644 index 0000000..1ca887f --- /dev/null +++ b/src/burp/api/montoya/ui/editor/extension/HttpResponseEditorProvider.java @@ -0,0 +1,24 @@ +/* + * Copyright (c) 2022-2023. PortSwigger Ltd. All rights reserved. + * + * This code may be used to extend the functionality of Burp Suite Community Edition + * and Burp Suite Professional, provided that this usage does not violate the + * license terms for those products. + */ + +package burp.api.montoya.ui.editor.extension; + +/** + * Extensions can register an instance of this interface to provide custom HTTP response editors within Burp's user interface. + */ +public interface HttpResponseEditorProvider +{ + /** + * Invoked by Burp when a new HTTP response editor is required from the extension. + * + * @param creationContext details about the context that is requiring a response editor + * + * @return An instance of {@link ExtensionProvidedHttpResponseEditor} + */ + ExtensionProvidedHttpResponseEditor provideHttpResponseEditor(EditorCreationContext creationContext); +} diff --git a/src/burp/api/montoya/ui/editor/extension/WebSocketMessageEditorProvider.java b/src/burp/api/montoya/ui/editor/extension/WebSocketMessageEditorProvider.java new file mode 100644 index 0000000..ca9a751 --- /dev/null +++ b/src/burp/api/montoya/ui/editor/extension/WebSocketMessageEditorProvider.java @@ -0,0 +1,16 @@ +package burp.api.montoya.ui.editor.extension; + +/** + * Extensions can register an instance of this interface to provide custom Web Socket message editors within Burp's user interface. + */ +public interface WebSocketMessageEditorProvider +{ + /** + * Invoked by Burp when a new Web Socket message editor is required from the extension. + * + * @param creationContext details about the context that is requiring a message editor + * + * @return An instance of {@link ExtensionProvidedWebSocketMessageEditor} + */ + ExtensionProvidedWebSocketMessageEditor provideMessageEditor(EditorCreationContext creationContext); +} diff --git a/src/burp/api/montoya/ui/menu/BasicMenuItem.java b/src/burp/api/montoya/ui/menu/BasicMenuItem.java new file mode 100644 index 0000000..29022ad --- /dev/null +++ b/src/burp/api/montoya/ui/menu/BasicMenuItem.java @@ -0,0 +1,41 @@ +package burp.api.montoya.ui.menu; + +import static burp.api.montoya.internal.ObjectFactoryLocator.FACTORY; + +public interface BasicMenuItem extends MenuItem +{ + /** + * The action performed when the {@link BasicMenuItem} is clicked. + */ + void action(); + + /** + * Create a copy of {@link BasicMenuItem} with a new {@link Runnable} action. + * + * @param action The new {@link Runnable} action. + * + * @return An updated copy of {@link BasicMenuItem}. + */ + BasicMenuItem withAction(Runnable action); + + /** + * Create a copy of {@link BasicMenuItem} with a new caption. + * + * @param caption The new caption. + * + * @return An updated copy of {@link BasicMenuItem} + */ + BasicMenuItem withCaption(String caption); + + /** + * Create a new instance of {@link BasicMenuItem} with a caption. + * + * @param caption The caption for the {@link BasicMenuItem}. + * + * @return A new instance of the {@link BasicMenuItem}. + */ + static BasicMenuItem basicMenuItem(String caption) + { + return FACTORY.basicMenuItem(caption); + } +} diff --git a/src/burp/api/montoya/ui/menu/Menu.java b/src/burp/api/montoya/ui/menu/Menu.java new file mode 100644 index 0000000..ae4e9aa --- /dev/null +++ b/src/burp/api/montoya/ui/menu/Menu.java @@ -0,0 +1,64 @@ +package burp.api.montoya.ui.menu; + +import java.util.List; + +import static burp.api.montoya.internal.ObjectFactoryLocator.FACTORY; + +/** + * A menu to be displayed in the {@link MenuBar}. + */ +public interface Menu +{ + /** + * The caption to be displayed for the menu. + * + * @return The caption + */ + String caption(); + + /** + * The list of {@link MenuItem} that will be displayed in the menu. + * + * @return The list of {@link MenuItem}. + */ + List menuItems(); + + /** + * Create a copy of {@link Menu} with a new caption. + * + * @param caption The new caption. + * + * @return An updated copy of {@link Menu}. + */ + Menu withCaption(String caption); + + /** + * Create a copy of {@link Menu} with one or more instances of {@link MenuItem}. + * + * @param menuItems One or more instances of {@link MenuItem}. + * + * @return An updated copy of {@link Menu}. + */ + Menu withMenuItems(MenuItem... menuItems); + + /** + * Create a copy of {@link Menu} with a new list of {@link MenuItem}. + * + * @param menuItems The new list of {@link MenuItem}. + * + * @return An updated copy of {@link Menu}. + */ + Menu withMenuItems(List menuItems); + + /** + * Create a new instance of {@link Menu}. + * + * @param caption The caption for the menu. + * + * @return A new instance of {@link Menu}. + */ + static Menu menu(String caption) + { + return FACTORY.menu(caption); + } +} diff --git a/src/burp/api/montoya/ui/menu/MenuBar.java b/src/burp/api/montoya/ui/menu/MenuBar.java new file mode 100644 index 0000000..b107af4 --- /dev/null +++ b/src/burp/api/montoya/ui/menu/MenuBar.java @@ -0,0 +1,31 @@ +package burp.api.montoya.ui.menu; + +import burp.api.montoya.core.Registration; + +import javax.swing.JMenu; + +/** + * The top menu bar for the main suite frame. + */ +public interface MenuBar +{ + /** + * Register a menu to be added to the menu bar. + * This option is available if you want more control over the menu structure. + * + * @param menu The menu to be registered. + * + * @return A {@link Registration} for the menu. + */ + Registration registerMenu(JMenu menu); + + /** + * Register a menu to be added to the menu bar. + * This option is available if you want to add a simple menu. + * + * @param menu The menu to be registered. + * + * @return A {@link Registration} for the menu. + */ + Registration registerMenu(Menu menu); +} diff --git a/src/burp/api/montoya/ui/menu/MenuItem.java b/src/burp/api/montoya/ui/menu/MenuItem.java new file mode 100644 index 0000000..af380a9 --- /dev/null +++ b/src/burp/api/montoya/ui/menu/MenuItem.java @@ -0,0 +1,28 @@ +package burp.api.montoya.ui.menu; + +import static burp.api.montoya.internal.ObjectFactoryLocator.FACTORY; + +/** + * An item to be displayed in a {@link Menu}. + */ +public interface MenuItem +{ + /** + * The caption of the {@link MenuItem}. + * + * @return The caption. + */ + String caption(); + + /** + * Create a new instance of {@link BasicMenuItem} with a caption. + * + * @param caption The caption for the {@link BasicMenuItem}. + * + * @return A new instance of the {@link BasicMenuItem}. + */ + static BasicMenuItem basicMenuItem(String caption) + { + return FACTORY.basicMenuItem(caption); + } +} diff --git a/src/burp/api/montoya/ui/swing/SwingUtils.java b/src/burp/api/montoya/ui/swing/SwingUtils.java new file mode 100644 index 0000000..2c7276f --- /dev/null +++ b/src/burp/api/montoya/ui/swing/SwingUtils.java @@ -0,0 +1,45 @@ +/* + * Copyright (c) 2022-2023. PortSwigger Ltd. All rights reserved. + * + * This code may be used to extend the functionality of Burp Suite Community Edition + * and Burp Suite Professional, provided that this usage does not violate the + * license terms for those products. + */ + +package burp.api.montoya.ui.swing; + +import burp.api.montoya.core.HighlightColor; + +import java.awt.Color; +import java.awt.Component; +import java.awt.Frame; +import java.awt.Window; + +/** + * This interface gives you access to swing utilities. + */ +public interface SwingUtils +{ + /** + * @return the main Burp suite frame. + */ + Frame suiteFrame(); + + /** + * Retrieve the top-level {@code Window} containing the supplied component. + * + * @param component the component. + * + * @return the top-level {@code Window} containing the component. + */ + Window windowForComponent(Component component); + + /** + * Convert a highlight color to a java color. + * + * @param highlightColor the {@link HighlightColor} + * + * @return the java color for the highlight color. + */ + Color colorForHighLight(HighlightColor highlightColor); +} diff --git a/src/burp/api/montoya/utilities/Base64DecodingOptions.java b/src/burp/api/montoya/utilities/Base64DecodingOptions.java new file mode 100644 index 0000000..b0d9aef --- /dev/null +++ b/src/burp/api/montoya/utilities/Base64DecodingOptions.java @@ -0,0 +1,20 @@ +/* + * Copyright (c) 2022-2023. PortSwigger Ltd. All rights reserved. + * + * This code may be used to extend the functionality of Burp Suite Community Edition + * and Burp Suite Professional, provided that this usage does not violate the + * license terms for those products. + */ + +package burp.api.montoya.utilities; + +/** + * This enum defines HTML encodings. + */ +public enum Base64DecodingOptions +{ + /** + * Decode using the URL and Filename safe type base64 transcoding scheme + */ + URL +} diff --git a/src/burp/api/montoya/utilities/Base64EncodingOptions.java b/src/burp/api/montoya/utilities/Base64EncodingOptions.java new file mode 100644 index 0000000..41ccce3 --- /dev/null +++ b/src/burp/api/montoya/utilities/Base64EncodingOptions.java @@ -0,0 +1,25 @@ +/* + * Copyright (c) 2022-2023. PortSwigger Ltd. All rights reserved. + * + * This code may be used to extend the functionality of Burp Suite Community Edition + * and Burp Suite Professional, provided that this usage does not violate the + * license terms for those products. + */ + +package burp.api.montoya.utilities; + +/** + * This enum defines HTML encodings. + */ +public enum Base64EncodingOptions +{ + /** + * Encode using the URL and Filename safe type base64 transcoding scheme + */ + URL, + + /** + * Encode without adding any padding characters at the end of the data. + */ + NO_PADDING +} diff --git a/src/burp/api/montoya/utilities/Base64Utils.java b/src/burp/api/montoya/utilities/Base64Utils.java new file mode 100644 index 0000000..845b904 --- /dev/null +++ b/src/burp/api/montoya/utilities/Base64Utils.java @@ -0,0 +1,93 @@ +/* + * Copyright (c) 2022-2023. PortSwigger Ltd. All rights reserved. + * + * This code may be used to extend the functionality of Burp Suite Community Edition + * and Burp Suite Professional, provided that this usage does not violate the + * license terms for those products. + */ + +package burp.api.montoya.utilities; + +import burp.api.montoya.core.ByteArray; + +import java.util.Base64; + +/** + * This interface contains various methods that give you access to base64 encoding and decoding features. + */ +public interface Base64Utils +{ + /** + * Encodes all bytes from the specified byte array into a newly-allocated + * byte array using the {@link Base64} encoding scheme. The returned byte + * array is of the length of the resulting bytes. + * + * @param data the byte array to encode + * @param options the options to use for encoding + * + * @return A newly-allocated byte array containing the resulting + * encoded bytes. + */ + ByteArray encode(ByteArray data, Base64EncodingOptions... options); + + /** + * Encodes all bytes from the specified String into a newly-allocated + * byte array using the {@link Base64} encoding scheme. The returned byte + * array is of the length of the resulting bytes. + * + * @param data the string to encode. + * @param options the options to use for encoding + * + * @return A newly-allocated byte array containing the resulting + * encoded bytes. + */ + ByteArray encode(String data, Base64EncodingOptions... options); + + /** + * Encodes all bytes from the specified byte array into a String using the {@link Base64} encoding scheme. + * + * @param data the byte array to encode + * @param options the options to use for encoding + * + * @return A newly-allocated byte array containing the resulting + * encoded bytes. + */ + String encodeToString(ByteArray data, Base64EncodingOptions... options); + + /** + * Encodes all bytes from the specified String into a String using the {@link Base64} encoding scheme. + * + * @param data the string to encode. + * @param options the options to use for encoding + * + * @return A newly-allocated byte array containing the resulting + * encoded bytes. + */ + String encodeToString(String data, Base64EncodingOptions... options); + + /** + * Decodes all bytes from the specified byte array into a newly-allocated + * byte array using the {@link Base64} decoding scheme. The returned byte + * array is of the length of the resulting bytes. + * + * @param data the bytes to decode. + * @param options the options to use for decoding + * + * @return A newly-allocated byte array containing the resulting + * decoded bytes. + */ + ByteArray decode(ByteArray data, Base64DecodingOptions... options); + + /** + * Decodes all bytes from the specified String into a newly-allocated + * byte array using the {@link Base64} decoding scheme. The returned byte + * array is of the length of the resulting bytes. + * + * @param data the string to decode. + * @param options the options to use for decoding + * + * @return A newly-allocated byte array containing the resulting + * decoded bytes. + */ + ByteArray decode(String data, Base64DecodingOptions... options); +} diff --git a/src/burp/api/montoya/utilities/ByteUtils.java b/src/burp/api/montoya/utilities/ByteUtils.java new file mode 100644 index 0000000..2e4b0ec --- /dev/null +++ b/src/burp/api/montoya/utilities/ByteUtils.java @@ -0,0 +1,156 @@ +/* + * Copyright (c) 2022-2023. PortSwigger Ltd. All rights reserved. + * + * This code may be used to extend the functionality of Burp Suite Community Edition + * and Burp Suite Professional, provided that this usage does not violate the + * license terms for those products. + */ + +package burp.api.montoya.utilities; + +import java.util.regex.Pattern; + +/** + * This interface gives you access to various methods for querying and manipulating byte arrays. + */ +public interface ByteUtils +{ + /** + * This method searches a piece of data for the first occurrence of a specified pattern. + * It works on byte-based data in a way that is similar to the way the native Java method {@link String#indexOf(String)} works on String-based data. + * + * @param data The data to be searched. + * @param searchTerm The value to be searched for. + * + * @return The offset of the first occurrence of the pattern within the specified bounds, or -1 if no match is found. + */ + int indexOf(byte[] data, byte[] searchTerm); + + /** + * This method searches a piece of data for the first occurrence of a specified pattern. + * It works on byte-based data in a way that is similar to the way the native Java method {@link String#indexOf(String)} works on String-based data. + * + * @param data The data to be searched. + * @param searchTerm The value to be searched for. + * @param caseSensitive Flags whether the search is case-sensitive. + * + * @return The offset of the first occurrence of the pattern within the specified bounds, or -1 if no match is found. + */ + int indexOf(byte[] data, byte[] searchTerm, boolean caseSensitive); + + /** + * This method searches a piece of data for the first occurrence of a specified pattern. + * It works on byte-based data in a way that is similar to the way the native Java method {@link String#indexOf(String)} works on String-based data. + * + * @param data The data to be searched. + * @param searchTerm The value to be searched for. + * @param caseSensitive Flags whether the search is case-sensitive. + * @param from The offset within data where the search should begin. + * @param to The offset within data where the search should end. + * + * @return The offset of the first occurrence of the pattern within the specified bounds, or -1 if no match is found. + */ + int indexOf(byte[] data, byte[] searchTerm, boolean caseSensitive, int from, int to); + + /** + * This method searches a piece of data for the first occurrence of a specified pattern. + * + * @param data The data to be searched. + * @param pattern The pattern to be matched. + * + * @return The offset of the first occurrence of the pattern within the specified bounds, or -1 if no match is found. + */ + int indexOf(byte[] data, Pattern pattern); + + /** + * This method searches a piece of data for the first occurrence of a specified pattern. + * + * @param data The data to be searched. + * @param pattern The pattern to be matched. + * @param from The offset within data where the search should begin. + * @param to The offset within data where the search should end. + * + * @return The offset of the first occurrence of the pattern within the specified bounds, or -1 if no match is found. + */ + int indexOf(byte[] data, Pattern pattern, int from, int to); + + /** + * This method searches a piece of data and counts all matches for a specified pattern. + * + * @param data The data to be searched. + * @param searchTerm The value to be searched for. + * + * @return The count of all matches of the pattern. + */ + int countMatches(byte[] data, byte[] searchTerm); + + /** + * This method searches a piece of data and counts all matches for a specified pattern. + * + * @param data The data to be searched. + * @param searchTerm The value to be searched for. + * @param caseSensitive Flags whether the search is case-sensitive. + * + * @return The count of all matches of the pattern. + */ + int countMatches(byte[] data, byte[] searchTerm, boolean caseSensitive); + + /** + * This method searches a piece of data and counts all matches for a specified pattern. + * + * @param data The data to be searched. + * @param searchTerm The value to be searched for. + * @param caseSensitive Flags whether the search is case-sensitive. + * @param from The offset within data where the search should begin. + * @param to The offset within data where the search should end. + * + * @return The count of all matches of the pattern within the specified bounds. + */ + int countMatches(byte[] data, byte[] searchTerm, boolean caseSensitive, int from, int to); + + /** + * This method searches a piece of data and counts all matches for a specified pattern. + * + * @param data The data to be searched. + * @param pattern The pattern to be matched. + * + * @return The count of all matches of the pattern within the specified bounds. + */ + int countMatches(byte[] data, Pattern pattern); + + /** + * This method searches a piece of data and counts all matches for a specified pattern. + * + * @param data The data to be searched. + * @param pattern The pattern to be matched. + * @param from The offset within data where the search should begin. + * @param to The offset within data where the search should end. + * + * @return The count of all matches of the pattern within the specified bounds. + */ + int countMatches(byte[] data, Pattern pattern, int from, int to); + + /** + * This method can be used to convert data from an array of bytes into String form. The conversion does not reflect any particular character set, and a byte with the + * representation 0xYZ will always be converted into a character with the hex representation 0x00YZ. It performs the opposite conversion to the method {@link ByteUtils#convertFromString(String)}, + * and byte-based data that is converted to a String and back again using these two methods is guaranteed to retain its integrity (which may not be the case with + * conversions that reflect a given character set). + * + * @param bytes The data to be converted. + * + * @return The converted data. + */ + String convertToString(byte[] bytes); + + /** + * This method can be used to convert data from String form into an array of bytes. The conversion does not reflect any particular character set, and a character with + * the hex representation 0xWXYZ will always be converted into a byte with the representation 0xYZ. It performs the opposite conversion to the method {@link ByteUtils#convertToString(byte[])}, + * and byte-based data that is converted to a String and back again using these two methods is guaranteed to retain its integrity (which may not be the case with + * conversions that reflect a given character set). + * + * @param string The data to be converted + * + * @return The converted data. + */ + byte[] convertFromString(String string); +} diff --git a/src/burp/api/montoya/utilities/CompressionType.java b/src/burp/api/montoya/utilities/CompressionType.java new file mode 100644 index 0000000..fa30f1e --- /dev/null +++ b/src/burp/api/montoya/utilities/CompressionType.java @@ -0,0 +1,19 @@ +/* + * Copyright (c) 2022-2023. PortSwigger Ltd. All rights reserved. + * + * This code may be used to extend the functionality of Burp Suite Community Edition + * and Burp Suite Professional, provided that this usage does not violate the + * license terms for those products. + */ + +package burp.api.montoya.utilities; + +/** + * This enum defines available compression types. + */ +public enum CompressionType +{ + GZIP, + DEFLATE, + BROTLI +} diff --git a/src/burp/api/montoya/utilities/CompressionUtils.java b/src/burp/api/montoya/utilities/CompressionUtils.java new file mode 100644 index 0000000..940cff8 --- /dev/null +++ b/src/burp/api/montoya/utilities/CompressionUtils.java @@ -0,0 +1,37 @@ +/* + * Copyright (c) 2022-2023. PortSwigger Ltd. All rights reserved. + * + * This code may be used to extend the functionality of Burp Suite Community Edition + * and Burp Suite Professional, provided that this usage does not violate the + * license terms for those products. + */ + +package burp.api.montoya.utilities; + +import burp.api.montoya.core.ByteArray; + +/** + * This interface gives you access to data compression features. + */ +public interface CompressionUtils +{ + /** + * Compress data using the specified compression type. + * + * @param data data to be compressed + * @param type {@link CompressionType} to use. Only GZIP is supported + * + * @return compressed data + */ + ByteArray compress(ByteArray data, CompressionType type); + + /** + * Decompress data compressed using the specified compression type. + * + * @param compressedData data to be decompressed + * @param type {@link CompressionType} of the compressed data + * + * @return decompressed data + */ + ByteArray decompress(ByteArray compressedData, CompressionType type); +} diff --git a/src/burp/api/montoya/utilities/CryptoUtils.java b/src/burp/api/montoya/utilities/CryptoUtils.java new file mode 100644 index 0000000..122681a --- /dev/null +++ b/src/burp/api/montoya/utilities/CryptoUtils.java @@ -0,0 +1,27 @@ +/* + * Copyright (c) 2022-2023. PortSwigger Ltd. All rights reserved. + * + * This code may be used to extend the functionality of Burp Suite Community Edition + * and Burp Suite Professional, provided that this usage does not violate the + * license terms for those products. + */ + +package burp.api.montoya.utilities; + +import burp.api.montoya.core.ByteArray; + +/** + * This interface gives you access to cryptographic features. + */ +public interface CryptoUtils +{ + /** + * Generate a message digest for the supplied data using the specified algorithm + * + * @param data the data to generate the digest from + * @param algorithm the message {@link DigestAlgorithm} to use + * + * @return the generated message digest + */ + ByteArray generateDigest(ByteArray data, DigestAlgorithm algorithm); +} diff --git a/src/burp/api/montoya/utilities/DigestAlgorithm.java b/src/burp/api/montoya/utilities/DigestAlgorithm.java new file mode 100644 index 0000000..7e8bbc5 --- /dev/null +++ b/src/burp/api/montoya/utilities/DigestAlgorithm.java @@ -0,0 +1,85 @@ +/* + * Copyright (c) 2022-2023. PortSwigger Ltd. All rights reserved. + * + * This code may be used to extend the functionality of Burp Suite Community Edition + * and Burp Suite Professional, provided that this usage does not violate the + * license terms for those products. + */ + +package burp.api.montoya.utilities; + +/** + * Enum of available message digest algorithms. + */ +public enum DigestAlgorithm +{ + BLAKE2B_160("BLAKE2B-160"), + BLAKE2B_256("BLAKE2B-256"), + BLAKE2B_384("BLAKE2B-384"), + BLAKE2B_512("BLAKE2B-512"), + BLAKE2S_128("BLAKE2S-128"), + BLAKE2S_160("BLAKE2S-160"), + BLAKE2S_224("BLAKE2S-224"), + BLAKE2S_256("BLAKE2S-256"), + BLAKE3_256("BLAKE3-256"), + DSTU7564_256("DSTU7564-256"), + DSTU7564_384("DSTU7564-384"), + DSTU7564_512("DSTU7564-512"), + GOST3411("GOST3411"), + GOST3411_2012_256("GOST3411-2012-256"), + GOST3411_2012_512("GOST3411-2012-512"), + HARAKA_256("HARAKA-256"), + HARAKA_512("HARAKA-512"), + KECCAK_224("KECCAK-224"), + KECCAK_256("KECCAK-256"), + KECCAK_288("KECCAK-288"), + KECCAK_384("KECCAK-384"), + KECCAK_512("KECCAK-512"), + MD2("MD2"), + MD4("MD4"), + MD5("MD5"), + PARALLEL_HASH_128_256("PARALLELHASH128-256"), + PARALLEL_HASH_256_512("PARALLELHASH256-512"), + RIPEMD_128("RIPEMD128"), + RIPEMD_160("RIPEMD160"), + RIPEMD_256("RIPEMD256"), + RIPEMD_320("RIPEMD320"), + SHA_1("SHA-1"), + SHA_224("SHA-224"), + SHA_256("SHA-256"), + SHA_384("SHA-384"), + SHA_512("SHA-512"), + SHA_512_224("SHA-512/224"), + SHA_512_256("SHA-512/256"), + SHA3_224("SHA3-224"), + SHA3_256("SHA3-256"), + SHA3_384("SHA3-384"), + SHA3_512("SHA3-512"), + SHAKE_128_256("SHAKE128-256"), + SHAKE_256_512("SHAKE256-512"), + SKEIN_1024_1024("SKEIN-1024-1024"), + SKEIN_1024_384("SKEIN-1024-384"), + SKEIN_1024_512("SKEIN-1024-512"), + SKEIN_256_128("SKEIN-256-128"), + SKEIN_256_160("SKEIN-256-160"), + SKEIN_256_224("SKEIN-256-224"), + SKEIN_256_256("SKEIN-256-256"), + SKEIN_512_128("SKEIN-512-128"), + SKEIN_512_160("SKEIN-512-160"), + SKEIN_512_224("SKEIN-512-224"), + SKEIN_512_256("SKEIN-512-256"), + SKEIN_512_384("SKEIN-512-384"), + SKEIN_512_512("SKEIN-512-512"), + SM3("SM3"), + TIGER("TIGER"), + TUPLEHASH_128_256("TUPLEHASH128-256"), + TUPLEHASH_256_512("TUPLEHASH256-512"), + WHIRLPOOL("WHIRLPOOL"); + + public final String displayName; + + DigestAlgorithm(String displayName) + { + this.displayName = displayName; + } +} diff --git a/src/burp/api/montoya/utilities/HtmlEncoding.java b/src/burp/api/montoya/utilities/HtmlEncoding.java new file mode 100644 index 0000000..c43c8f1 --- /dev/null +++ b/src/burp/api/montoya/utilities/HtmlEncoding.java @@ -0,0 +1,36 @@ +/* + * Copyright (c) 2022-2023. PortSwigger Ltd. All rights reserved. + * + * This code may be used to extend the functionality of Burp Suite Community Edition + * and Burp Suite Professional, provided that this usage does not violate the + * license terms for those products. + */ + +package burp.api.montoya.utilities; + +/** + * This enum defines HTML encodings. + */ +public enum HtmlEncoding +{ + /** + * Encode only HTML special characters. + */ + STANDARD, + + /** + * Encode HTML special characters as per STANDARD, + * encode all other characters as decimal entities. + */ + ALL_CHARACTERS, + + /** + * Encode all characters as decimal entities. + */ + ALL_CHARACTERS_DECIMAL, + + /** + * Encode all characters as hex entities. + */ + ALL_CHARACTERS_HEX +} diff --git a/src/burp/api/montoya/utilities/HtmlUtils.java b/src/burp/api/montoya/utilities/HtmlUtils.java new file mode 100644 index 0000000..6a28a0a --- /dev/null +++ b/src/burp/api/montoya/utilities/HtmlUtils.java @@ -0,0 +1,43 @@ +/* + * Copyright (c) 2022-2023. PortSwigger Ltd. All rights reserved. + * + * This code may be used to extend the functionality of Burp Suite Community Edition + * and Burp Suite Professional, provided that this usage does not violate the + * license terms for those products. + */ + +package burp.api.montoya.utilities; + +/** + * This interface gives you access to HTML encoding and decoding features. + */ +public interface HtmlUtils +{ + /** + * Encode HTML text using {@link HtmlEncoding#STANDARD} encoding. + * + * @param html {@code String} to be encoded. + * + * @return the encoded {@code String}. + */ + String encode(String html); + + /** + * Encode HTML text. + * + * @param html {@code String} to be encoded. + * @param encoding {@link HtmlEncoding} to be used. + * + * @return the encoded {@code String}. + */ + String encode(String html, HtmlEncoding encoding); + + /** + * Decode encoded HTML text. + * + * @param encodedHtml {@code String} to be decoded. + * + * @return the decoded {@code String}. + */ + String decode(String encodedHtml); +} diff --git a/src/burp/api/montoya/utilities/NumberUtils.java b/src/burp/api/montoya/utilities/NumberUtils.java new file mode 100644 index 0000000..6072e48 --- /dev/null +++ b/src/burp/api/montoya/utilities/NumberUtils.java @@ -0,0 +1,154 @@ +/* + * Copyright (c) 2022-2023. PortSwigger Ltd. All rights reserved. + * + * This code may be used to extend the functionality of Burp Suite Community Edition + * and Burp Suite Professional, provided that this usage does not violate the + * license terms for those products. + */ + +package burp.api.montoya.utilities; + +import burp.api.montoya.core.ByteArray; + +/** + * This interface gives you access to number string conversion features. + */ +public interface NumberUtils +{ + /** + * @param binaryString the binary string to convert + * + * @return string containing the octal representation + */ + String convertBinaryToOctal(String binaryString); + + /** + * @param byteArray the byte array to convert + * + * @return string containing the octal representation + */ + String convertBinaryToOctal(ByteArray byteArray); + + /** + * @param binaryString the binary string to convert + * + * @return string containing the decimal representation + */ + String convertBinaryToDecimal(String binaryString); + + /** + * @param byteArray the byte array to convert + * + * @return string containing the decimal representation + */ + String convertBinaryToDecimal(ByteArray byteArray); + + /** + * @param binaryString the binary string to convert + * + * @return string containing the hex representation + */ + String convertBinaryToHex(String binaryString); + + /** + * @param byteArray the byte array to convert + * + * @return string containing the hex representation + */ + String convertBinaryToHex(ByteArray byteArray); + + /** + * @param octalString the octal string to convert + * + * @return string containing the binary representation + */ + String convertOctalToBinary(String octalString); + + /** + * @param octalString the octal string to convert + * + * @return string containing the decimal representation + */ + String convertOctalToDecimal(String octalString); + + /** + * @param octalString the octal string to convert + * + * @return string containing the hex representation + */ + String convertOctalToHex(String octalString); + + /** + * @param decimalString the decimal string to convert + * + * @return string containing the binary representation + */ + String convertDecimalToBinary(String decimalString); + + /** + * @param decimalString the decimal string to convert + * + * @return string containing the octal representation + */ + String convertDecimalToOctal(String decimalString); + + /** + * @param decimalString the decimal string to convert + * + * @return string containing the hex representation + */ + String convertDecimalToHex(String decimalString); + + /** + * @param hexString the hex string to convert + * + * @return string containing the binary representation + */ + String convertHexToBinary(String hexString); + + /** + * @param hexString the hex string to convert + * + * @return string containing the octal representation + */ + String convertHexToOctal(String hexString); + + /** + * @param hexString the hex string to convert + * + * @return string containing the decimal representation + */ + String convertHexToDecimal(String hexString); + + /** + * @param binaryString the binary string to convert + * @param radix the radix to convert to + * + * @return string containing the representation in the specified radix + */ + String convertBinary(String binaryString, int radix); + + /** + * @param octalString the octal string to convert + * @param radix the radix to convert to + * + * @return string containing the representation in the specified radix + */ + String convertOctal(String octalString, int radix); + + /** + * @param decimalString the decimal string to convert + * @param radix the radix to convert to + * + * @return string containing the representation in the specified radix + */ + String convertDecimal(String decimalString, int radix); + + /** + * @param hexString the hex string to convert + * @param radix the radix to convert to + * + * @return string containing the representation in the specified radix + */ + String convertHex(String hexString, int radix); +} diff --git a/src/burp/api/montoya/utilities/RandomUtils.java b/src/burp/api/montoya/utilities/RandomUtils.java new file mode 100644 index 0000000..bc00bd2 --- /dev/null +++ b/src/burp/api/montoya/utilities/RandomUtils.java @@ -0,0 +1,89 @@ +/* + * Copyright (c) 2022-2023. PortSwigger Ltd. All rights reserved. + * + * This code may be used to extend the functionality of Burp Suite Community Edition + * and Burp Suite Professional, provided that this usage does not violate the + * license terms for those products. + */ + +package burp.api.montoya.utilities; + +import java.util.Arrays; +import java.util.stream.Collectors; + +public interface RandomUtils +{ + /** + * Generate a random string using alphanumeric characters + * + * @param length length of the resulting random string + * + * @return randomly generated string + */ + String randomString(int length); + + /** + * Generate a random string using the supplied characters + * + * @param length length of the resulting random string + * @param chars the characters to use to generate the string + * + * @return randomly generated string + */ + String randomString(int length, String chars); + + /** + * Generate a random string using the supplied {@link CharacterSet} + * + * @param length length of the resulting random string + * @param characterSets the list {@code CharacterSet} to use to generate the string + * + * @return randomly generated string + */ + String randomString(int length, CharacterSet... characterSets); + + /** + * Generate a random string using the supplied characters + * + * @param minLength the inclusive minimum length of the generated string + * @param maxLength the inclusive maximum length of the generated string + * @param chars the characters to use to generate the string + * + * @return randomly generated string + */ + String randomString(int minLength, int maxLength, String chars); + + /** + * Generate a random string using the supplied {@link CharacterSet} + * + * @param minLength the inclusive minimum length of the generated string + * @param maxLength the inclusive maximum length of the generated string + * @param characterSets the list {@code CharacterSet} to use to generate the string + * + * @return randomly generated string + */ + String randomString(int minLength, int maxLength, CharacterSet... characterSets); + + enum CharacterSet + { + ASCII_LOWERCASE("abcdefghijklmnopqrstvwxyz"), + ASCII_UPPERCASE("ABCDEFGHIJKLMNOPQRSTVWXYZ"), + ASCII_LETTERS(ASCII_LOWERCASE, ASCII_UPPERCASE), + DIGITS("0123456789"), + PUNCTUATION("!\"#$%&'()*+,-./:;=<>?@[\\]^_`{|}~."), + WHITESPACE(" \t\n\u000b\r\f"), + PRINTABLE(DIGITS, ASCII_LETTERS, PUNCTUATION, WHITESPACE); + + public final String characters; + + CharacterSet(String characters) + { + this.characters = characters; + } + + CharacterSet(CharacterSet... charsList) + { + characters = Arrays.stream(charsList).map(charSet -> charSet.characters).collect(Collectors.joining()); + } + } +} diff --git a/src/burp/api/montoya/utilities/StringUtils.java b/src/burp/api/montoya/utilities/StringUtils.java new file mode 100644 index 0000000..6ae6739 --- /dev/null +++ b/src/burp/api/montoya/utilities/StringUtils.java @@ -0,0 +1,35 @@ +/* + * Copyright (c) 2022-2023. PortSwigger Ltd. All rights reserved. + * + * This code may be used to extend the functionality of Burp Suite Community Edition + * and Burp Suite Professional, provided that this usage does not violate the + * license terms for those products. + */ + +package burp.api.montoya.utilities; + +/** + * This interface gives you access to String manipulation features. + */ +public interface StringUtils +{ + /** + * Convert a string to the hex values of its ASCII characters. + * Each character will be converted to a two digit hex value. + * + * @param data The ASCII data to convert. + * + * @return The string of hex values. + */ + String convertAsciiToHexString(String data); + + /** + * Convert a string of hex values to a string of ASCII characters. + * Each pair of hex digits will be converted to a single ASCII character. + * + * @param data The string of hex values to convert. + * + * @return The string of ASCII characters. + */ + String convertHexStringToAscii(String data); +} diff --git a/src/burp/api/montoya/utilities/URLUtils.java b/src/burp/api/montoya/utilities/URLUtils.java new file mode 100644 index 0000000..f6c8d4f --- /dev/null +++ b/src/burp/api/montoya/utilities/URLUtils.java @@ -0,0 +1,53 @@ +/* + * Copyright (c) 2022-2023. PortSwigger Ltd. All rights reserved. + * + * This code may be used to extend the functionality of Burp Suite Community Edition + * and Burp Suite Professional, provided that this usage does not violate the + * license terms for those products. + */ + +package burp.api.montoya.utilities; + +import burp.api.montoya.core.ByteArray; + +/** + * This interface gives you access to URL encoding and decoding features. + */ +public interface URLUtils +{ + /** + * @param string {@code String} to be url encoded. + * + * @return the url encoded {@code String}. + * + * @see java.net.URLEncoder#encode(String, String) + */ + String encode(String string); + + /** + * @param string the {@code String} to be url decoded + * + * @return the url decoded {@code String} + * + * @see java.net.URLDecoder#decode(String, String) + */ + String decode(String string); + + /** + * @param byteArray {@link ByteArray} to be url encoded. + * + * @return the url encoded {@link ByteArray}. + * + * @see java.net.URLEncoder#encode(String, String) + */ + ByteArray encode(ByteArray byteArray); + + /** + * @param byteArray the {@link ByteArray} to be url decoded + * + * @return the url decoded {@link ByteArray} + * + * @see java.net.URLDecoder#decode(String, String) + */ + ByteArray decode(ByteArray byteArray); +} diff --git a/src/burp/api/montoya/utilities/Utilities.java b/src/burp/api/montoya/utilities/Utilities.java new file mode 100644 index 0000000..c896979 --- /dev/null +++ b/src/burp/api/montoya/utilities/Utilities.java @@ -0,0 +1,60 @@ +/* + * Copyright (c) 2022-2023. PortSwigger Ltd. All rights reserved. + * + * This code may be used to extend the functionality of Burp Suite Community Edition + * and Burp Suite Professional, provided that this usage does not violate the + * license terms for those products. + */ + +package burp.api.montoya.utilities; + +/** + * This interface gives you access to other interfaces that have various data conversion and querying features. + */ +public interface Utilities +{ + /** + * @return an instance of {@link burp.api.montoya.utilities.Base64Utils} + */ + Base64Utils base64Utils(); + + /** + * @return an instance of {@link burp.api.montoya.utilities.ByteUtils} + */ + ByteUtils byteUtils(); + + /** + * @return an instance of {@link burp.api.montoya.utilities.CompressionUtils} + */ + CompressionUtils compressionUtils(); + + /** + * @return an instance of {@link burp.api.montoya.utilities.CryptoUtils} + */ + CryptoUtils cryptoUtils(); + + /** + * @return an instance of {@link burp.api.montoya.utilities.HtmlUtils} + */ + HtmlUtils htmlUtils(); + + /** + * @return an instance of {@link burp.api.montoya.utilities.NumberUtils} + */ + NumberUtils numberUtils(); + + /** + * @return an instance of {@link burp.api.montoya.utilities.RandomUtils} + */ + RandomUtils randomUtils(); + + /** + * @return an instance of {@link burp.api.montoya.utilities.StringUtils} + */ + StringUtils stringUtils(); + + /** + * @return an instance of {@link burp.api.montoya.utilities.URLUtils} + */ + URLUtils urlUtils(); +} diff --git a/src/burp/api/montoya/websocket/BinaryMessage.java b/src/burp/api/montoya/websocket/BinaryMessage.java new file mode 100644 index 0000000..f10812d --- /dev/null +++ b/src/burp/api/montoya/websocket/BinaryMessage.java @@ -0,0 +1,24 @@ +/* + * Copyright (c) 2022-2023. PortSwigger Ltd. All rights reserved. + * + * This code may be used to extend the functionality of Burp Suite Community Edition + * and Burp Suite Professional, provided that this usage does not violate the + * license terms for those products. + */ + +package burp.api.montoya.websocket; + +import burp.api.montoya.core.ByteArray; + +public interface BinaryMessage +{ + /** + * @return Binary based WebSocket payload. + */ + ByteArray payload(); + + /** + * @return The direction of the message. + */ + Direction direction(); +} diff --git a/src/burp/api/montoya/websocket/BinaryMessageAction.java b/src/burp/api/montoya/websocket/BinaryMessageAction.java new file mode 100644 index 0000000..98dc62c --- /dev/null +++ b/src/burp/api/montoya/websocket/BinaryMessageAction.java @@ -0,0 +1,76 @@ +/* + * Copyright (c) 2023. PortSwigger Ltd. All rights reserved. + * + * This code may be used to extend the functionality of Burp Suite Community Edition + * and Burp Suite Professional, provided that this usage does not violate the + * license terms for those products. + */ + +package burp.api.montoya.websocket; + +import burp.api.montoya.core.ByteArray; + +import static burp.api.montoya.internal.ObjectFactoryLocator.FACTORY; + +/** + * Binary WebSocket message. + */ +public interface BinaryMessageAction +{ + /** + * @return The action associated with this message. + */ + MessageAction action(); + + /** + * @return The payload of this message. + */ + ByteArray payload(); + + /** + * Build a binary WebSocket message to be processed. + * + * @param payload The binary message payload. + * + * @return The {@link BinaryMessageAction} containing the message to be processed. + */ + static BinaryMessageAction continueWith(ByteArray payload) + { + return FACTORY.continueWithBinaryMessage(payload); + } + + /** + * Build a binary WebSocket message to be processed. + * + * @param binaryMessage The binary message payload. + * + * @return The {@link BinaryMessageAction} containing the message to be processed. + */ + static BinaryMessageAction continueWith(BinaryMessage binaryMessage) + { + return FACTORY.continueWithBinaryMessage(binaryMessage.payload()); + } + + /** + * Build a binary WebSocket message to be dropped. + * + * @return The {@link BinaryMessageAction} dropping the message. + */ + static BinaryMessageAction drop() + { + return FACTORY.dropBinaryMessage(); + } + + /** + * Build a binary websocket message action. + * + * @param payload the binary payload for the message + * @param action the action to take for the message. + * + * @return The {@link BinaryMessageAction} containing the message and the action. + */ + static BinaryMessageAction binaryMessageAction(ByteArray payload, MessageAction action) + { + return FACTORY.binaryMessageAction(payload, action); + } +} diff --git a/src/burp/api/montoya/websocket/Direction.java b/src/burp/api/montoya/websocket/Direction.java new file mode 100644 index 0000000..de6589e --- /dev/null +++ b/src/burp/api/montoya/websocket/Direction.java @@ -0,0 +1,18 @@ +/* + * Copyright (c) 2022-2023. PortSwigger Ltd. All rights reserved. + * + * This code may be used to extend the functionality of Burp Suite Community Edition + * and Burp Suite Professional, provided that this usage does not violate the + * license terms for those products. + */ + +package burp.api.montoya.websocket; + +/** + * This enum is used to indicate the direction of the WebSocket message. + */ +public enum Direction +{ + CLIENT_TO_SERVER, + SERVER_TO_CLIENT +} diff --git a/src/burp/api/montoya/websocket/MessageAction.java b/src/burp/api/montoya/websocket/MessageAction.java new file mode 100644 index 0000000..56fc53a --- /dev/null +++ b/src/burp/api/montoya/websocket/MessageAction.java @@ -0,0 +1,25 @@ +/* + * Copyright (c) 2023. PortSwigger Ltd. All rights reserved. + * + * This code may be used to extend the functionality of Burp Suite Community Edition + * and Burp Suite Professional, provided that this usage does not violate the + * license terms for those products. + */ + +package burp.api.montoya.websocket; + +/** + * This enum represents the action to be applied to a {@link TextMessageAction} or {@link BinaryMessageAction}. + */ +public enum MessageAction +{ + /** + * Causes Burp to forward the message. + */ + CONTINUE, + + /** + * Causes Burp to drop the message. + */ + DROP +} diff --git a/src/burp/api/montoya/websocket/MessageHandler.java b/src/burp/api/montoya/websocket/MessageHandler.java new file mode 100644 index 0000000..4f23ff3 --- /dev/null +++ b/src/burp/api/montoya/websocket/MessageHandler.java @@ -0,0 +1,44 @@ +/* + * Copyright (c) 2022-2023. PortSwigger Ltd. All rights reserved. + * + * This code may be used to extend the functionality of Burp Suite Community Edition + * and Burp Suite Professional, provided that this usage does not violate the + * license terms for those products. + */ + +package burp.api.montoya.websocket; + +/** + * This interface allows an extension to be notified when messages are received or the WebSocket has been closed. + */ +public interface MessageHandler +{ + /** + * Invoked when a text message is sent or received from the application. + * This gives the extension the ability to modify the message before it is + * sent to the application or processed by Burp. + * + * @param textMessage Intercepted text based WebSocket message. + * + * @return The message. + */ + TextMessageAction handleTextMessage(TextMessage textMessage); + + /** + * Invoked when a binary message is sent or received from the application. + * This gives the extension the ability to modify the message before it is + * sent to the application or processed by Burp. + * + * @param binaryMessage Intercepted binary based WebSocket message. + * + * @return The message. + */ + BinaryMessageAction handleBinaryMessage(BinaryMessage binaryMessage); + + /** + * Invoked when the WebSocket is closed. + */ + default void onClose() + { + } +} diff --git a/src/burp/api/montoya/websocket/TextMessage.java b/src/burp/api/montoya/websocket/TextMessage.java new file mode 100644 index 0000000..f1cb3f3 --- /dev/null +++ b/src/burp/api/montoya/websocket/TextMessage.java @@ -0,0 +1,22 @@ +/* + * Copyright (c) 2022-2023. PortSwigger Ltd. All rights reserved. + * + * This code may be used to extend the functionality of Burp Suite Community Edition + * and Burp Suite Professional, provided that this usage does not violate the + * license terms for those products. + */ + +package burp.api.montoya.websocket; + +public interface TextMessage +{ + /** + * @return Text based WebSocket payload. + */ + String payload(); + + /** + * @return The direction of the message. + */ + Direction direction(); +} diff --git a/src/burp/api/montoya/websocket/TextMessageAction.java b/src/burp/api/montoya/websocket/TextMessageAction.java new file mode 100644 index 0000000..f3f0a6e --- /dev/null +++ b/src/burp/api/montoya/websocket/TextMessageAction.java @@ -0,0 +1,74 @@ +/* + * Copyright (c) 2023. PortSwigger Ltd. All rights reserved. + * + * This code may be used to extend the functionality of Burp Suite Community Edition + * and Burp Suite Professional, provided that this usage does not violate the + * license terms for those products. + */ + +package burp.api.montoya.websocket; + +import static burp.api.montoya.internal.ObjectFactoryLocator.FACTORY; + +/** + * Text WebSocket message. + */ +public interface TextMessageAction +{ + /** + * @return The action associated with this message. + */ + MessageAction action(); + + /** + * @return The payload of this message. + */ + String payload(); + + /** + * Build a text WebSocket message to be processed. + * + * @param payload The text message payload. + * + * @return The {@link TextMessageAction} containing the message to be processed. + */ + static TextMessageAction continueWith(String payload) + { + return FACTORY.continueWithTextMessage(payload); + } + + /** + * Build a text WebSocket message to be processed. + * + * @param textMessage the text message payload + * + * @return The {@link TextMessageAction} containing the message to be processed. + */ + static TextMessageAction continueWith(TextMessage textMessage) + { + return FACTORY.continueWithTextMessage(textMessage.payload()); + } + + /** + * Build a text WebSocket message to be dropped. + * + * @return The {@link TextMessageAction} dropping the message. + */ + static TextMessageAction drop() + { + return FACTORY.dropTextMessage(); + } + + /** + * Build a websocket text message action. + * + * @param payload the binary payload for the message + * @param action the action to take for the message. + * + * @return The {@link TextMessageAction} containing the message and the action. + */ + static TextMessageAction textMessageAction(String payload, MessageAction action) + { + return FACTORY.textMessageAction(payload, action); + } +} diff --git a/src/burp/api/montoya/websocket/WebSocket.java b/src/burp/api/montoya/websocket/WebSocket.java new file mode 100644 index 0000000..2214c44 --- /dev/null +++ b/src/burp/api/montoya/websocket/WebSocket.java @@ -0,0 +1,46 @@ +/* + * Copyright (c) 2022-2023. PortSwigger Ltd. All rights reserved. + * + * This code may be used to extend the functionality of Burp Suite Community Edition + * and Burp Suite Professional, provided that this usage does not violate the + * license terms for those products. + */ + +package burp.api.montoya.websocket; + +import burp.api.montoya.core.ByteArray; +import burp.api.montoya.core.Registration; + +/** + * WebSocket within Burp. + */ +public interface WebSocket +{ + /** + * This method allows an extension to send a text message via the WebSocket. + * + * @param message The message to be sent. + */ + void sendTextMessage(String message); + + /** + * This method allows an extension to send a binary message via the WebSocket. + * + * @param message The message to be sent. + */ + void sendBinaryMessage(ByteArray message); + + /** + * This method will close the WebSocket. + */ + void close(); + + /** + * Register a handler which will perform an action when a message is sent to or received from the application. + * + * @param handler An object created by the extension that implements {@link MessageHandler} interface. + * + * @return The {@link Registration} for the handler. + */ + Registration registerMessageHandler(MessageHandler handler); +} diff --git a/src/burp/api/montoya/websocket/WebSocketCreated.java b/src/burp/api/montoya/websocket/WebSocketCreated.java new file mode 100644 index 0000000..e8e0e4d --- /dev/null +++ b/src/burp/api/montoya/websocket/WebSocketCreated.java @@ -0,0 +1,30 @@ +/* + * Copyright (c) 2022-2023. PortSwigger Ltd. All rights reserved. + * + * This code may be used to extend the functionality of Burp Suite Community Edition + * and Burp Suite Professional, provided that this usage does not violate the + * license terms for those products. + */ + +package burp.api.montoya.websocket; + +import burp.api.montoya.core.ToolSource; +import burp.api.montoya.http.message.requests.HttpRequest; + +public interface WebSocketCreated +{ + /** + * @return The WebSocket that was created. + */ + WebSocket webSocket(); + + /** + * @return The HTTP upgrade request that initiated the WebSocket creation. + */ + HttpRequest upgradeRequest(); + + /** + * @return Indicates which Burp tool that created the WebSocket. + */ + ToolSource toolSource(); +} diff --git a/src/burp/api/montoya/websocket/WebSocketCreatedHandler.java b/src/burp/api/montoya/websocket/WebSocketCreatedHandler.java new file mode 100644 index 0000000..2b02379 --- /dev/null +++ b/src/burp/api/montoya/websocket/WebSocketCreatedHandler.java @@ -0,0 +1,23 @@ +/* + * Copyright (c) 2022-2023. PortSwigger Ltd. All rights reserved. + * + * This code may be used to extend the functionality of Burp Suite Community Edition + * and Burp Suite Professional, provided that this usage does not violate the + * license terms for those products. + */ + +package burp.api.montoya.websocket; + +/** + * Extensions can implement this interface and then call {@link WebSockets#registerWebSocketCreatedHandler} to register a WebSocket handler. + * The handler will be notified of new WebSockets created by any Burp tool. + */ +public interface WebSocketCreatedHandler +{ + /** + * Invoked by Burp when an application WebSocket has been created. + * + * @param webSocketCreated {@link WebSocketCreated} containing information about the application websocket that is being created. + */ + void handleWebSocketCreated(WebSocketCreated webSocketCreated); +} \ No newline at end of file diff --git a/src/burp/api/montoya/websocket/WebSockets.java b/src/burp/api/montoya/websocket/WebSockets.java new file mode 100644 index 0000000..3f1bdfa --- /dev/null +++ b/src/burp/api/montoya/websocket/WebSockets.java @@ -0,0 +1,48 @@ +/* + * Copyright (c) 2022-2023. PortSwigger Ltd. All rights reserved. + * + * This code may be used to extend the functionality of Burp Suite Community Edition + * and Burp Suite Professional, provided that this usage does not violate the + * license terms for those products. + */ + +package burp.api.montoya.websocket; + +import burp.api.montoya.core.Registration; +import burp.api.montoya.http.HttpService; +import burp.api.montoya.http.message.requests.HttpRequest; +import burp.api.montoya.websocket.extension.ExtensionWebSocketCreation; + +/** + * Provides access to WebSocket related functionality of Burp. + */ +public interface WebSockets +{ + /** + * Register a handler which will be invoked whenever a WebSocket is created by any Burp tool. + * + * @param handler An object created by the extension that implements {@link WebSocketCreatedHandler} interface. + * + * @return The {@link Registration} for the handler. + */ + Registration registerWebSocketCreatedHandler(WebSocketCreatedHandler handler); + + /** + * Create a new WebSocket using the specified service and path. + * + * @param service An {@link HttpService} specifying the target host + * @param path path for the upgrade HTTP request + * + * @return The {@link ExtensionWebSocketCreation} result. + */ + ExtensionWebSocketCreation createWebSocket(HttpService service, String path); + + /** + * Create a new WebSocket using the specified upgrade request. + * + * @param upgradeRequest The {@link HttpRequest} upgrade request + * + * @return The {@link ExtensionWebSocketCreation} result. + */ + ExtensionWebSocketCreation createWebSocket(HttpRequest upgradeRequest); +} diff --git a/src/burp/api/montoya/websocket/extension/ExtensionWebSocket.java b/src/burp/api/montoya/websocket/extension/ExtensionWebSocket.java new file mode 100644 index 0000000..e59d92c --- /dev/null +++ b/src/burp/api/montoya/websocket/extension/ExtensionWebSocket.java @@ -0,0 +1,46 @@ +/* + * Copyright (c) 2023. PortSwigger Ltd. All rights reserved. + * + * This code may be used to extend the functionality of Burp Suite Community Edition + * and Burp Suite Professional, provided that this usage does not violate the + * license terms for those products. + */ + +package burp.api.montoya.websocket.extension; + +import burp.api.montoya.core.ByteArray; +import burp.api.montoya.core.Registration; + +/** + * A WebSocket created via the Extension API. + */ +public interface ExtensionWebSocket +{ + /** + * This method allows an extension to send a text message via the WebSocket. + * + * @param message The message to be sent. + */ + void sendTextMessage(String message); + + /** + * This method allows an extension to send a binary message via the WebSocket. + * + * @param message The message to be sent. + */ + void sendBinaryMessage(ByteArray message); + + /** + * This method will close the WebSocket. + */ + void close(); + + /** + * Register an interface that is notified when messages arrive from the server. + * + * @param handler An object created by the extension that implements {@link ExtensionWebSocketMessageHandler} interface. + * + * @return The {@link Registration} for the handler. + */ + Registration registerMessageHandler(ExtensionWebSocketMessageHandler handler); +} diff --git a/src/burp/api/montoya/websocket/extension/ExtensionWebSocketCreation.java b/src/burp/api/montoya/websocket/extension/ExtensionWebSocketCreation.java new file mode 100644 index 0000000..3ee639c --- /dev/null +++ b/src/burp/api/montoya/websocket/extension/ExtensionWebSocketCreation.java @@ -0,0 +1,40 @@ +/* + * Copyright (c) 2023. PortSwigger Ltd. All rights reserved. + * + * This code may be used to extend the functionality of Burp Suite Community Edition + * and Burp Suite Professional, provided that this usage does not violate the + * license terms for those products. + */ + +package burp.api.montoya.websocket.extension; + +import burp.api.montoya.http.message.responses.HttpResponse; + +import java.util.Optional; + +/** + * Result of a WebSocket creation attempt + */ +public interface ExtensionWebSocketCreation +{ + /** + * The status of the WebSocket creation attempt. + * + * @return The {@link ExtensionWebSocketCreationStatus} creation status + */ + ExtensionWebSocketCreationStatus status(); + + /** + * The created WebSocket. + * + * @return the created {@link ExtensionWebSocket} + */ + Optional webSocket(); + + /** + * The HTTP response from the WebSocket creation attempt. + * + * @return the {@link HttpResponse} + */ + Optional upgradeResponse(); +} diff --git a/src/burp/api/montoya/websocket/extension/ExtensionWebSocketCreationStatus.java b/src/burp/api/montoya/websocket/extension/ExtensionWebSocketCreationStatus.java new file mode 100644 index 0000000..b846a76 --- /dev/null +++ b/src/burp/api/montoya/websocket/extension/ExtensionWebSocketCreationStatus.java @@ -0,0 +1,55 @@ +/* + * Copyright (c) 2023. PortSwigger Ltd. All rights reserved. + * + * This code may be used to extend the functionality of Burp Suite Community Edition + * and Burp Suite Professional, provided that this usage does not violate the + * license terms for those products. + */ + +package burp.api.montoya.websocket.extension; + +/** + * Status of a WebSocket creation attempt + */ +public enum ExtensionWebSocketCreationStatus +{ + /** + * WebSocket creation was successful. + */ + SUCCESS, + + /** + * Specified host was invalid. + */ + INVALID_HOST, + + /** + * Unable to resolve address for specified host. + */ + UNKNOWN_HOST, + + /** + * Specified port was invalid. + */ + INVALID_PORT, + + /** + * Unable to connect to specified host. + */ + CONNECTION_FAILED, + + /** + * Specified upgrade request was invalid. + */ + INVALID_REQUEST, + + /** + * Server returned a non-upgrade response. + */ + NON_UPGRADE_RESPONSE, + + /** + * Specified endpoint is configured for streaming responses. + */ + STREAMING_RESPONSE +} diff --git a/src/burp/api/montoya/websocket/extension/ExtensionWebSocketMessageHandler.java b/src/burp/api/montoya/websocket/extension/ExtensionWebSocketMessageHandler.java new file mode 100644 index 0000000..09228b4 --- /dev/null +++ b/src/burp/api/montoya/websocket/extension/ExtensionWebSocketMessageHandler.java @@ -0,0 +1,39 @@ +/* + * Copyright (c) 2023. PortSwigger Ltd. All rights reserved. + * + * This code may be used to extend the functionality of Burp Suite Community Edition + * and Burp Suite Professional, provided that this usage does not violate the + * license terms for those products. + */ + +package burp.api.montoya.websocket.extension; + +import burp.api.montoya.websocket.BinaryMessage; +import burp.api.montoya.websocket.TextMessage; + +/** + * This interface allows an extension to be notified when messages are received or the WebSocket has been closed. + */ +public interface ExtensionWebSocketMessageHandler +{ + /** + * Invoked when a text message is received from the application. + * + * @param textMessage text WebSocket message. + */ + void textMessageReceived(TextMessage textMessage); + + /** + * Invoked when a binary message is received from the application. + * + * @param binaryMessage binary WebSocket message. + */ + void binaryMessageReceived(BinaryMessage binaryMessage); + + /** + * Invoked when the WebSocket is closed. + */ + default void onClose() + { + } +} diff --git a/src/gui/JLabelLink.java b/src/gui/JLabelLink.java deleted file mode 100644 index 9c3e723..0000000 --- a/src/gui/JLabelLink.java +++ /dev/null @@ -1,102 +0,0 @@ -package gui; - -import java.awt.Cursor; -import java.awt.Desktop; -import java.awt.event.MouseAdapter; -import java.awt.event.MouseEvent; -import java.awt.image.BufferedImage; -import java.io.ByteArrayInputStream; -import java.io.IOException; -import java.net.URI; -import java.util.Base64; -import java.util.Objects; - -import javax.imageio.ImageIO; -import javax.swing.BoxLayout; -import javax.swing.ImageIcon; -import javax.swing.JFrame; -import javax.swing.JLabel; -import javax.swing.JPanel; -import javax.swing.border.EmptyBorder; - -import app.helpers.Output; - -public class JLabelLink extends JFrame { - private static final String LOGO_DATA = "iVBORw0KGgoAAAANSUhEUgAAAd0AAACKCAYAAADvwZCVAAAABmJLR0QA/wD/AP+gvaeTAAAACXBIWXMAAAsTAAALEwEAmpwYAAAAB3RJTUUH4QQYDA4ppMr3sQAAAB1pVFh0Q29tbWVudAAAAAAAQ3JlYXRlZCB3aXRoIEdJTVBkLmUHAAAgAElEQVR42u2deZRddZXvP/t37q2q1JCpEhLIUBkqlTAIiIwOD31ii4227cOgYEv60W1aW6ENEen13norL93vvT8YBKUdlwqI3SIRbRrbVlkOtAOKQRlDhgqZU5nnpKZ7zn5/nFMSioTce865t+45tT9r1R+QqnPP2fd3ft/f3r/92xsMwzAMwzAMwzAMwzAMwzAMwzAMwzAMwzAMwzAMwzAMwzAMwzAMwzAMwzAMwzAMwzAMwzAMwzAMwzAMwzAMwzAMwzAMwzAMwzAMwzAMwzAMwzAMwzAMwzAMwzAMwzAMwzAMwzAMwzAMwzAMwzAMwxjNSAbvuTFn34EPlOrANgEwmFEbJnnu/lP8e0NG35NqU4rGbqZYe2Nn47xqfsCO7pKsqI1ddBkN7OsckbG5DhjY0a19E9CX9hNcsyJ7Y8FEtzwmAR8DpuboO3gc+E4kekmYCnwUmBzn/QWeA+4H+jJmv2nA3wLjY/ztAeDLwOaT/PsE4G+AGTZVvGq8PAr8KEs3vXPJvPO8gvsg4sZWyy5+KXh0ymfWVN0uKxdT7Bi34CMicvZIfP+CBgEEogwq9Ktor1OOoBxUdH8QyO5BBvYUShyU0pGjp31h91EJx82op5Cx+x0LXAfMz9kE9t0URLcNuAZYEPPvfwg8mEHRnQrcAEyJ8bdbomc+GS2RTc+3qeJVY3Y78GMyMpGuXzxnnFf0Poq4jwCuWnbxCrKtFouR01qmF8TJe0DeOTIDQEKPTULPTaI1hzgZAAY8T/s9mo5ogV0iTZv2fbq9e7fqamBNKRjcPHXzS3tllHrHhYzet4X7Xs0A0BvTNgo0VXEyqiZNgBfzuUvAsTLGmo23V4+XTDF+XMOfqsh/ExGvakbR2ptFROpnbIoUgIJAc/TKTBFhLnCZooGIHgP2FaWxe++c+U/uvoUnpM//ffuO7p7RJMAFmz9yw2AkunFpJLt7/HEXC/2cek/XyDgHPjlvTsnJIpDJZo2R0mNxIK1AK8JMVblchAPa7F7cO+vMH/UsLT12eECf7rqnO/fvo7PhYKIb0RB5jFn0dOOO46Okl8Rm1CErF1McKHp/AfLWuvIKTYQ9cdLuxL1ZHP+r6HnfHN9U+D89S+ZdrAszOQ+Z6JroVvYOAMXox0TXyA0d4+a/2Tn3FyLSaNaoVwV2DeK8TufcJ4vFwtf2zZp/86aPzzjDRNeodwYSerpe5O1mjSTh5aNgRx3yyoEl0yeCWyQw16yRCe+3IM6dg3PLWlpabtvz6c5LTXSNvHq6EO7vZ1F0zdM1XsUycH6x9WpxvBcRm+ey5fm2IO5apOG2XbcseDc5S2S0wZgfgkhE4h49Go2ie8Q83Xxy09I5ZwUifwkyzqyRSa/XAW92Ist33bLgKhNdo15JKrpZ3NNNGl42TzdnbFjU0aSu+CGBiyx5KtPCK+LcBc7J3++8ef4bTXSNekMjzy2u6GZ1T9fCy8YraJnU8HYV71oRVzRr5EJ+3+gKctOWGzunm+ga9cYR4hcuyGp4uZF4R500YWTAqEN2fXLm6eIKN4gw06yRI49X5D1NTd61eThOZKJrnm6WPV2JPN04IUQ/El0jJywDR3HMNeK40sLKuZPeMeK8D+6aM+8cE12j3jzd0bSn64hfSatkopsvPrG06w1O3PUgY8waZa7UQ/wKfgIdgXqX0SLqdR7ee3VZtispZvHm9wC76uA+2oB6ermTerpZDC97xG/rV67oHgYO1eBZWlNaBPcRntmuJkENPqMidt/S3ibiLULkPPNyK5o29mug3xORA+WJH54qDYFqsxPGKjIZ1dOBdkTaJKy/XEW9knfs3z/nPnhps4lubdgE3FgH4lAkbCf3AeqrdOJoE12XQHTLCS/vBv6RsLtVNWkHlgJdCa8zADwEfJ/q71U/Tx01PlDa/9QJV1PFhgZ5RJRdiH9n+5h1a8r9mxWrkIXgrZva2TTeG2hxXsOEwBXmOPQNqrwFcecB7WkvfkREUM4NmrwLOXk7ThPdlPGBp+vgPorA1XVon17iZ+NmcU/XI9zTrZan2w88VoPnmAb8VUrRjmeBhxlFCWLbl87qEHHXKzIlySyvqjravGQFgpLny/KKx4sP3QOEUaAeYNUy+MHHbpo92WssXibO+4AqVwLj0rSpQkug3iW6jH+T5dk8eWB7uvkiSdecJF5jXj3dLM+lo4KViyk2FBo/5MT91ySTu6r2AXtGYr8yLyyHYOrnNuycfPvafy0eOfgJCYJbFX0+ZZs6J3Lu/p45LVm1k4luvhggWRP6rLX3S7qne8SGTLaZ2TbvMtR9WBMsGFVV0eDHaPBrs2g6jPv89r0/2bj6a5SC/4nqU2kJb3h6iI5Sa8MUE12jXjzduKKb5PjNSIpu3PBy0lrVxgiz6WMzJ3ietwiRrkQhTNX1GgRfR2SHWTU9rlmBP+nONd8X9e8QdFta11VkklfQSSa6Rh483ayJbpLwch/WwD6zKMiY1jHvU5E/lyQNDVT7UX3A80o/N6umj4Ae2tX/CPAdVU0rz6DVV5loomuY6I6MpxtXdI9hJSAzS8/SzjOdyCKQCfH1VlVVf+YFwQNHBgYGzKrVYfb9m/pKg/7DaGoZx06cjtWMdh8y0c2f6CZpZD+aPF0T3Yyy9kYai65wHSKXJsuM1Z0B/r0T7ly7waxaXRr8/udBn03FexYQ1cwWQDHRNU93uKebpTGRZE/Xmh1klPbGrreJJ9eKuNhH3FS1pBp8a/DA4e+bRavP3Z/ddEjRZ1U1cStNVZVAXWaPdpno5tPTjZMpmNVEqrgTr3m6GWTHTbOnBM7dgMrsBJO2gj7pB3rftK/0HDOrVp/lEAjBRpFETsHQVKVO/cxuB5jo5osgEpMknm6WRLeB+AVeTHQzxjJw0tS4UETelTCsfFACvX/KHWufN6vWDt93+1VTeecUOCwZPY9uopsvktZfzproxm3rBxZezhwfv2Xu+R5yPUjswgiqGkgQPNLnH/yuWFvHGouNDpKOUParuANZtUPBhkLuOMzoCS/H3YNW83Szxc6/ndwqFBch8vqEZQVf9IPg69M+07PHrFpj0XUp5Ywo+z3VvSa6Rj15unFXk2mI7pgKXqwSyc7KWgP70ULzxHeKuIWJutio9krg3//zzet+ZQatORKoThIoJp1gFHaWSn27TXSNPIhuY8KVaDPwd8C5Zf7+74AvED/jOu7K2RrYZ4i9N8+doc4tQpgaX2/DUo99funBa1bgm1VrPDEtQ/b2eh0krO+uqoEQrDuyX/ab6Br1IrpJwssNCcdEJ/Dfgbll/v5ZwKPAuhp7uj5WdzkT/OxyCoFXvE5ErkgYVt6sol+b/pn1W8yqtefgrpnjGMs5iaqHRe+uwB9mzdpk2ctG3ZDE0y0Qti2MgwAXErapc2X+zAJen+BZk3q61lGmzjnrDV2X4mSRiMQuhqAaDGjg/8uRHX2PmUVHhr6mhvkgZyW9jqB7fS09FaMVoYmuUTVPN2kj+7ii2whcRmXFKtqANxP/rG1cTzfAwst1z77Fc8Z5BfdhUZkfX3BVBZ4QCR6Yff+mPrPqCExKC/GKRe9dwIxE1wkPWD/V5LsXsmwPE938cSzy5OJQTCC6M4CLY4ypiyD2Xl0TFl7O6+pRgrEN7wV3tbgkDQ3YV/KD+9pvW7farDpCi6fZ8y9Rce9LlAQHCPQT6E/a2tbuM9E16ok+4icmJfF0zwM6Kn+P6ATOTuDpWiJVDtlzy/wuPPlLhNjdZFTVR4KHvH7/u2JbCSPCtpvmzFSVj4GcneQ6qqoB+uzAgP/DLIeWTXTzST/xj+HEFd0i8EbCcHGlTIj+Ns4q2DzdHPL8QhocXAu8KW7yVJSt/CwD/n2T7uk+ZFatPZuXzJvW0NRwM869P4UEqn4C/7u/3L5ubdbtYtnLJrrDxTOO6E4FLo25iHPAJcBEYFeNPN1BwPb36pSpszsvR7zrRCR2QwOBI6p6f/vYdSvNorVFQXZ9at55zhU+LiIfEpGmRNcLF1A/9Qd1RR6Oe5no5lN0ax1ePpswTBxzfuQsYH6FoiuR6MbxhHoJm0MYdUbPR+ecplK8AeiMfT5INQg0+MEY7ft21kORmRLbZbj9R2bN2OMarvLEG2q96CW/sG5Q1S9PvXvdS3mwk4muebpJRddFXu6EBPd8WnSNX1F+5rUjflu/3sjbNerMQ9rTWrhakKuShZX1pSDg6613bNphVq3u97Vj6ZTmQl/DOG1q6tzbW3gLHleIcBFIc8Jz1QAEGhxw6Ofbm9f8IC92M9E10T2eIpUf32kn3JP1Eo7DSwn3hA9WILpxq9uY6NYhu5bMO7fgedcrtCaQgkFR/vm0Tat/bhY9NSK0iqfv2fvpBdtO9bu+IqKBJ540oDJ2D0wuwkxtlU4P6VBlMoKXhtiqqopwBNUvHDgw8LX22/JTJ91EN3/UOnu5izA8nORFE+B8YDbwdA083WNYeLmu6Fk6pcV5bpEiFybxclX5uV8a+KassO+3PG9Vpolz/xuRU2Z3ewAqAuIQLYAU5bgvS9JslaJ6IAiCLzX6etfcr7x0ME82N9HNH8c3spcY46FS0b2MMDyclDMIz/k+Q3nHOzzzdHM0Eem4dzhxCzVBxESgJ9Dgq1PuWt9tFi3X0xUBaa3gD/5o7Wq0I4u2BzYqwReODPR9dfJnNx3Im83tyFAeF6/xz6B6VBZeHk9YUaqYwn03EIapmyoYu7anmwP2LJk3TQpukSLTEni5PoE+5Pcc+IFZNKszl/ai+u8u8JdsOrDms7NzKLjm6eZXdI/E9HSlQiGbA7wuxQXghYSVrdaW+ftJwssmuvUwWBfi7fHctU7cn5AorBysDPzSN07/5k4repIpnVUV9Jgqz4oGD4tfemhizptSmOjmU3TTaGRfzt9fTBgWTotZkfCWK7qNMe1jDezrhH0dCy4WT65HpDnBkD9IoPdObut+xiyaLcFV1e2gD6o/+K0dm9c/d84o2Is30TXRHU65otsKvImE/TGHMYZwj/g7nDrRKa6nOxR+t7KAIz1Qb5nfttfx4STdZ1Q1UNV/7e89/B25w87kZg0nTAC5Cq+xc+rsBb/dd2vwi8OH+l7o+OLm/Xl9ZhPdfIpu3PZ+EglfOUwnzDhO9R0krE41Fdh8it+Nm0jlk6z9oZHOIJU9qu8Vde8XJwmSp3St+nr/9M9v32tWzRZRElczsAB0PrirAqSnua35N7tu6Xq41Bf89Ix7unfnbqFhX31uPd14c9jLnu6peANhODjtJMZOwuYJ5YzdOJ6utfWrA3YtmTsX5xYhMimBl9sb+Hr/8yvX/NIsmn0BFpGCiJsh4t7vxPtisanwhV1L51219sZUo2kmukbVRDdpePlUv3MZ0FyF+28jLJRRKGPsxnkZA/N0R3iALqboFYvXiri3JKo8RfAj6S99822P2/587gTYuQni3NXOK/7T+MYFf7/nxs7pJrpGPYtutcPLUwgTnqpxVG+oOlV7Gb8X56hSkCASYKTAnraut0SF8BN4MLpFA7130j3dW82ieRZfmeU8dytjCv9v55J55+XhuUx080kv8bNzy/F0zyOsRCVVuv8zo5/XoiES3krvYSi8bJ7uCLBt8emTxHM3KNIVW241GAR9sLd09DGz6KgQ3zGIu04K3j9uX9p1kYmuUY/0Ea/+cjnndIuEoeWxVbz/dsLjSK81PhuJV71oKLxs1BgFKY4d9z4V955kYWV+Pdg/+I2Zd23tNauOGuH1nHNXNXjuf+z95NyzTXSNvIguhOHl15oQJxBmGFdz7BQJjyONq4Lo+lgi1Yiwc8mcc5yTRRLu28dV7n34+vXT717/glk0hYVQyDFUj5bzo6rHVLVXVfs00FK0CKqV8DrEvVsbGm7afmPn5Kza3I4M5Vd04xwyLyd7+RzC/rnleipB9LtS4X2cA8wFVqYsukO1qY0asnnJ9DGFYsOHES5O4OUGEDzsDg08YhZNScigRwP/8z6U2QZRnKAFFWly0KaqE1A5TUTOAGYoMgkYKyJVWZSLSAH0umKjt+5nl3N3FpPoTHTzSZL2fq8VXvaovHfuxsh7Pr3C+zidMFnrZKLbEFN04y5IjASMcS1XKPJBJy5Wne7Qo9JnoXTvxJx1nRlhT/eQwsOn3b5mbdl/c/x/LEO295zeJO0tLQ0DTFZx83DuYtS9GdFzQcal0epvmO63Oo8bzr543m95fN0vTHSNrHu6ryVmbYRNCQoVvJ8/BCYCH6jQ2x1D2EzhAU4cDo7r6Zro1pidn+qYirjrRST2sQ+BI34Q3De5uftJs2jKwuuLUkFi4Ste4uUo9BwjLK26G1ily3h0b2/nGRJ4l6mT96vKn6Qtvop0Cd7CbYtPf2raV3qOZcnetqebX9GN6+k2cPKjOAsIM5fLfXkOA/8J/Crm/bwemHmSf4srur0mujWc0BfieTLmWufxrkR9cgn+g8Heh2S5lXqsd2Q5waTbure2375mxcCx0ic00FtBnwq3B1L6DBFPRN7T0Db24qzZx0Q3nyQJLxdPIroOuAiopIJQD/Ac8PtoFVwpM4ELUhZd83RryN7Z896Ak+tVEzQ0UN0QlIL7Trt7c49ZNFuccU/37knNq78aBPopCH6YpvAiMkMc71y5OJXWoia6RiIGInGJk1lY4MTh42bC0HIlBQ1WAduAbsrrHDScFsIQc9NJPHIT3TrmxRva26BwPci58b3cYEAkeGBy69qfmEWz6/medvvqxyn5/yDws7QynkNv1719WltXpqpV2Z5uPklSX/hknu6cyOssd/IsAU8BhyJx/B1weYwxdyFhUtUG83SzRfukiVeJcDUxM1lVVUX5tfr+Qxs3drgNi2L3Tz71Z/VpE0W8FMq9FDYs6qj4PmfNaglk+apcj8tJd6777e5b5n9OxM1GZE4q3xt0Fpy87gTzg4muUXPRjdvI/kSiK4T7q5X0zj0Yia5GAvxkdE/jK1nMEh4bel2Komt7ujVg56c6pjoKixSZElfHRAhAxqorfqx1SrGq50FFtAByaQoXurJ1SnPFZ0j3HtP1e27svHfSPd2H8jwu9vf5P5rQKA+K01tAUggLyzgHFyg8KhmpMmeim0+G6i+n5ek2EoaWWyq4zmZeGVJ+HthSoehCWPnqjcB/AIMJRdca2NdqYnHN7QjnkChjVTxFzxcn51f/jiWUzAT3KyKCcilCHPH+pd84+G3CyFBu6bqnu3/PknmPqJMPikvu7YoTR+CdtffGzjYysmAx0c2/pxtHdIePi+mEZRnLnZAUeJYwkWqIrcDThEUvKpnYXPTZ7bx8gF+I12EoSTMIo2Jji0sara1WkYUqusyxnlkJRk1+zTF6n2um9QlVnZ3OMSKd1dTY0JyVBYslUpmne6KF2HDRPQfoqOAa/YSh5b5h/++3VB7aFV7dAMERJlLFaXZgomsYI8iMsVv7UZ4ipYiTQvsx6WvLyvOb6ObTdkk93ePDyx5hreVxFVxjP/DMsM8PCI8O7Y1xT+2ElbC8YYuDOIsRa+tnGCMZDFhOEMA6QftTuZ5IE4iJ7iigoc5FN24j++F9aidy6o4/w1lPeExoOGuBF2Le0yVA63Heb1zRNU/XMEYY1WC/IqlUklLU86TQlJVnN9GNubji1N14RnRME79n7PA93XnA/Ar+3icMLZ+oGMZB4DdUHlYaaoDQkVB0kyxGDMNICc/pICmFlwUR3w8yk59kohvfbuNTEt2gSiIQN0t3eHj5ImByhZ/7BK/MNB6iRFgSMk7B+jMIjy2l4ekahjGC+IEUSSmRV0E9z2XmRIKJbjxaCMshSvLxwmCVRLfvJMJXzpgYCp23ETasr+Q83QZO3hkIwqzmF2I8cyNhmLshoeiap2sYI4yImwDaksq1wPe11Geim28mV+j9vZaXe6xKIhC3CMTxx3GmEYZ1K3me3xAeDzoZewizmIMYY/WCyO5xRdeP7G0Yxgihy3AInSCp5MUEMIgfZCaCZaIbjzlU3h/2RJSAA1W6x6SiK4QdhWZU4NH3Ab88xecOEoaY45ypmweclUB0+3nlMSbDMGrMxo0dDS5cQHtpXE/Qg04LR7Py/Ca6leMR1gMek8K1BgkTjuopvAxhg4FGXpkxXNb7RFju8VTP8zSwJsZzj41sX0gguv02hA1j5BgzsWke4i5Oq/CJKDv6Bo5aeDnHtBN2vkkjCWCAsAtPvXm6TdFzVtLgICBsarCljN/dGf1upaJbIEzsaolp/yR9hg3DSIguxPMKXAnMSuV6qqrCpsEjRfN0c8xbCLNo08hcPsDLpQ3rxdMdEt35QFcFzzkA/DoS+3Lu7ZdUXqhCCJsfzCZeaMo8XcMYQfZ3dJ3tnHs/KdU5EChpEKybff8m83SrdK8zYUQbFo8H/jzyAtNgM2FiUb15us1U3rB+C2GCVLne6x+Al2J4u1MJ95rjlIE0T9cwRogNf9cxXp27QZEL0qm5DIoeVoIXsmSHLDU8aAb+hrDE4AOEIcpacxXwTtI7n/ti9DzVYCAS3jjt/doJk8UKZY/9UEQr6Wm5lfBoUaUdZMYQHh2KU4HGEqlqRFDyfSl6L6GalWxxUZgBUowrCKqqKPtEtPJ3WtkSqBfkdTxsW3x6c0Nj018rcr2IpKc7yhYtBWtNdKvHnEj4zge+zqkzZdPkPOAjFXp/p/K6niJ+slM5oh5nwhOgk8q6AQ0QZiRXEi7uBX4BXEdlLQOHEtniiKeFl2vEz7asXffWuQv+Sgay0T3HOdpx3l0IFybSANUHtOR/udK/Kwn9U+98aXcex8K+W+eMU238iApLQcandV1VVeB3/fRvy5I9stjarw24ljCz9nvAdwizYaspvh3AJwkTqNIq/bgzuu9qFWrwiVd9SSJRO62Cv+khPJ9b6bOsBDYRHgOqhE7K2zse7o1bA/sacc0KfFidGQ9k281dkxq95GNDRHdN+sza1TYCQEH2LZ1ztmrDX6uwSMSNT/UDhEMaBD+fedfW3izZJav9dF008d4MvBv4PvDvhCHOtCsOzQeWRh6Zl9545Le8ssl7NTzdoeL+lSwUPMIzyF4Fz/IcJ25wcCo2EWYxn1nhPY4hDC9XugA6Gi1GDMOoVpTjcgpnXdTZsUcK7xSR6xC5JNWQ8h+9XH2hEOivs2afrDex96IJuwv4AGH27I8jQdvIy3uacRgDvB1YDFxJuglcvcBPqW4d4CASmbh2rcSj/hXxinwcA/4TWEi4Z1+pR17pQsdKQBrGMBoTeLIsQ1iFbJk+vaG1oWHy4GDxTOfxJhF3hQjnKtKcVtLUsJe/P1AeGb957WYT3ZET35mE1ZPeTZgV/BRhyPO5SIB3E1aACjhxCcKhKkfjCcOrVwJ/RhhaTnvQPAf8pMoCEDe8XCm7osVO3CSQlYSZz/OrfJ/WS9cwXjXryZiBIm/fd+uCeWVPLCV1AUFxj0iz9MpEZsm0ZqEjUJnrFaUDmDDk2VarDVugwTPS7z8qK7IXuSrkbQgRVlA6K/KAFxKWG9xK2ON1PWFIc2f0/4eSmFp4uc7wecDZhH1kvSrcYz/wSHQf1WTI0622Z7cKSLKHtYFwW6CL6rZKNE/XMF7NTCdyu2r5r54rgOBJ9L46BA/wquHRnvhNDo5IoP/cPn7dmiwavJDjwTRU5KGJMCno9ZH3N+TpDh4nuo2E4WOPcL+4WoNHI8/uYaq/txjUwNP1CTOQk5w1PhJd472kU1rTRNcwynZ0RYCWSme8kWokrqqBKN8d6PcflDvI5BGrwmgaX8Oet2kE7uEw8C3iJR3Vo6e7h3A/N8lnKGEyVQ/hkTATXcMwTiS4quiTpWDgS2fcsz6zx6usDGTtCAjDyiugZiu0uI3sy2U1YXg5KWuBZ2ogiCa6hpFRwYWg26l/15Q71v8my89iolujMUMYVv4SYeJRraim6PqECVRpPM9hql/o5PgjVIZhZElwVTcF6t++4cDa70nG32ET3dqwCfgc8ESNP7eX6lW82h+Jbhp70wFhS8BqhoxKxD9CZRjGCAmuoGsC9f/vrg3r7r/wK1Wbz2pGwb7WqtMDfIYwrFzrFdqxKopuN/BsitdbFV1vGtXJ0xjA6i4bRpYE10eDX5V8/7OrVq77t7c9XtWtMhPdPIyZSHDvAL7KyJQf7KU64eUg8tp7UrzmQcKkrHdQnU5SVnfZMDIzeepBVV2BP/ilKXeufypPz2aiWz3BXQvcDdw3gh5WtTzdw4Sh5TSv7UdCvp/K6j5XIrrm6RpG/Xq2iuqgKr9H/W8U/WPfHn/X1n15e04T3fQpAY8D9xDWgx7JkEi1RHeooEXaPEcYYn476YeYzdM1jPoV3H5UV4M+LEHp4fY7u1+UnCY9Zkl0FdgbiUixTu9vG/Bt4F6gHhorVyORKiAsr7m1Cvc7lJz11iqMTfN0DaP+PNsDIM9roN8P/NKPthxdtyoPyVJ5Ed2jwBcJj6j8GWGpxzidZtIWWgiL/f8UeAB4jHh9bKvBUPKQpmin3kgYq7FHfXzzhEkpX9s8XcMYSYH945yph1DZAPqEHwQ/LQSlJ9vb1m+T5dmsMJVn0SXyHlcDD0Xe0JXABcDU6FlqJcAaeXw7IgF6OBLdequS4pP+MZnNhBWkqhX6eTb6nv9Lyt+nebqGUSOBFQhVVrWEyEFF94jSrYE+g+jvAvVfONjP1q57ukfdQjiLe7p+JLxD4nsmYXP5N0be7xmEtZTTrqGs0WcfAF6MPLLHCPc299exrQ6R3r7yUB/gajZr2EcYvr6EdBtOHIa6DluVUri/QRgd3kIVv4PSy05ZLH+udvZXSorWdEwLqiCqoIKWFEoSRr0GQHpV9ZjAXkV7VHUzyEZhcINqsLEw4O8ev33T4Sx2BkrXhvnAAeMI2/udG/2cBcwmDFO28cpmBnKC59dhXqxGk9h+wq5E3cDTwO8jT2xnnU/iRM98ebQQSUt0V1GdJKrj6YxEN83xuSOKRtSjKDUCV1LcGm0AAAETSURBVAATEl4niL6bF00/K2PlYoqzx3ddEYjXHnel5wNO+UP7bS9WPZ9DQfZ+esEVTmRKTQ0ViPrqqxPnq+igE/r9UtArQXBsUPVw0ZeDhZaBo+MG+wc4vHNAcr4/O5pFdzgNhB1rpkQ/06KfKZEIj+flzkIumqwGCEOxBwgL+W8/7qeHMImrD0b3Ks0wDMMw0S3X65Nh3q47TnSDYV5ugIXqDMMwDMMwDMMwDMMwDMMwDMMwDMMwDMMwDMMwDMMwDMMwDMMwDMMwDMMwDMMwDMMwDMMwDMMwDMMwDMMwDMMwDMMwDMMwDMMwDMMwDMMwDMMwDMMwDMMwDMMwDMMwDMMwDMMwDMMwDMMwDMMwjPzz/wEu4k8eiN1t8wAAAABJRU5ErkJggg=="; - private static final Base64.Decoder DECODER = Base64.getDecoder(); - private static final long serialVersionUID = 1L; - private final JPanel pan; - - public JLabelLink(String title, int x, int y) { - this.setTitle(title); - this.setSize(x, y); - this.setDefaultCloseOperation(JFrame.DISPOSE_ON_CLOSE); - this.setBounds(0, 0, x, y); - this.setLocationRelativeTo(null); - this.setLocationRelativeTo(null); - - pan = new JPanel(); - pan.setBorder(new EmptyBorder(10, 10, 10, 10)); - BoxLayout boxLayout = new BoxLayout(pan, BoxLayout.Y_AXIS); - pan.setLayout(boxLayout); - - this.setContentPane(pan); - this.setVisible(true); - } - - public void addURL(String content, String tooltip) { - JLabel label = new JLabel("" + content + ""); - label.putClientProperty("html.disable", null); - label.setCursor(new Cursor(Cursor.HAND_CURSOR)); - label.setToolTipText(tooltip); - addMouseHandler(label); - pan.add(label); - } - - public void addText(String content) { - JLabel label = new JLabel("" + content + ""); - label.putClientProperty("html.disable", null); - pan.add(label); - } - - public void addLogoImage() { - byte[] imageBytes = DECODER.decode(LOGO_DATA); - try { - BufferedImage img = ImageIO.read(new ByteArrayInputStream(imageBytes)); - ImageIcon icon = new ImageIcon(img); - JLabel label = new JLabel(); - label.setIcon(icon); - pan.add(label); - } catch (IOException e) { - e.printStackTrace(); - } - } - - private void addMouseHandler(final JLabel website) { - website.addMouseListener(new MouseAdapter() { - - @Override - public void mouseClicked(MouseEvent e) { - try { - String href = parseHREF(website.getText()); - Desktop.getDesktop().browse(new URI(Objects.requireNonNull(href))); - } catch (Exception ex) { - Output.outputError("Exception trying to browser from jlabel href - "+ex.getMessage()); - } - } - }); - } - - private static String parseHREF(String html) { - String hrefMarker = "href=\""; - int hrefLoc = html.indexOf(hrefMarker); - if (hrefLoc > 1) { - int hrefEndLoc = html.indexOf("\">"); - if (hrefEndLoc > hrefLoc + 4) { - return html.substring(hrefLoc + hrefMarker.length(), hrefEndLoc); - } - } - return null; - } - -} diff --git a/src/gui/JWTInterceptTab.java b/src/gui/JWTInterceptTab.java deleted file mode 100644 index 75a76da..0000000 --- a/src/gui/JWTInterceptTab.java +++ /dev/null @@ -1,400 +0,0 @@ -package gui; - -import java.awt.Color; -import java.awt.Dimension; -import java.awt.GridBagConstraints; -import java.awt.GridBagLayout; -import java.awt.GridLayout; -import java.awt.SystemColor; -import java.awt.Toolkit; -import java.awt.datatransfer.StringSelection; -import java.awt.event.ActionListener; -import java.awt.event.KeyListener; -import java.util.Enumeration; - -import javax.swing.AbstractButton; -import javax.swing.ButtonGroup; -import javax.swing.JButton; -import javax.swing.JCheckBox; -import javax.swing.JComboBox; -import javax.swing.JLabel; -import javax.swing.JPanel; -import javax.swing.JPopupMenu; -import javax.swing.JRadioButton; -import javax.swing.JScrollPane; -import javax.swing.JTextArea; -import javax.swing.ScrollPaneConstants; -import javax.swing.SwingConstants; -import javax.swing.SwingUtilities; -import javax.swing.UIManager; -import javax.swing.event.DocumentListener; -import javax.swing.text.JTextComponent; - -import org.fife.ui.rsyntaxtextarea.RSyntaxTextArea; -import org.fife.ui.rsyntaxtextarea.Style; -import org.fife.ui.rsyntaxtextarea.SyntaxConstants; -import org.fife.ui.rsyntaxtextarea.SyntaxScheme; -import org.fife.ui.rsyntaxtextarea.Token; -import org.fife.ui.rsyntaxtextarea.TokenTypes; -import org.fife.ui.rtextarea.RTextScrollPane; - -import app.controllers.ReadableTokenFormat; -import app.helpers.Config; -import app.helpers.Output; -import model.JWTInterceptModel; -import model.Strings; - -public class JWTInterceptTab extends JPanel { - - private static final long serialVersionUID = 1L; - private static final String HTML_DISABLE = "html.disable"; - private final JWTInterceptModel jwtIM; - private final RSyntaxTextAreaFactory rSyntaxTextAreaFactory; - private JRadioButton rdbtnRecalculateSignature; - private JRadioButton rdbtnRandomKey; - private JRadioButton rdbtnOriginalSignature; - private JRadioButton rdbtnChooseSignature; - - private JTextArea jwtKeyArea; - - private RSyntaxTextArea jwtHeaderArea; - private RSyntaxTextArea jwtPayloadArea; - private RSyntaxTextArea jwtSignatureArea; - - private JLabel lblSecretKey; - private JRadioButton rdbtnDontModifySignature; - private JLabel lblProblem; - private JComboBox noneAttackComboBox; - private JLabel lblNewLabel; - private JLabel lblCookieFlags; - private JLabel lbRegisteredClaims; - private JCheckBox chkbxCVEAttack; - private JButton btnCopyPubPrivKeyCVEAttack; - private ButtonGroup btgrp; - - public JWTInterceptTab(JWTInterceptModel jwtIM, RSyntaxTextAreaFactory rSyntaxTextAreaFactory) { - this.jwtIM = jwtIM; - this.rSyntaxTextAreaFactory = rSyntaxTextAreaFactory; - drawGui(); - } - - public void registerActionListeners(ActionListener dontMofiy, - ActionListener randomKeyListener, - ActionListener originalSignatureListener, - ActionListener recalculateSignatureListener, - ActionListener chooseSignatureListener, - ActionListener algAttackListener, - ActionListener cveAttackListener, - DocumentListener syncKey, - KeyListener jwtAreaTyped) { - rdbtnDontModifySignature.addActionListener(dontMofiy); - rdbtnRecalculateSignature.addActionListener(randomKeyListener); - rdbtnOriginalSignature.addActionListener(originalSignatureListener); - rdbtnChooseSignature.addActionListener(chooseSignatureListener); - rdbtnRandomKey.addActionListener(recalculateSignatureListener); - noneAttackComboBox.addActionListener(algAttackListener); - chkbxCVEAttack.addActionListener(cveAttackListener); - jwtKeyArea.getDocument().addDocumentListener(syncKey); - jwtHeaderArea.addKeyListener(jwtAreaTyped); - jwtPayloadArea.addKeyListener(jwtAreaTyped); - } - - private void drawGui() { - setLayout(new GridLayout(1, 2, 10, 0)); - - JPanel areasPanel = new JPanel(); - areasPanel.setLayout(new GridLayout(3, 1)); - - JPanel actionPanel = new JPanel(); - actionPanel.setLayout(new GridBagLayout()); - GridBagConstraints c = new GridBagConstraints(); - c.gridx = 0; - c.gridy = 0; - c.anchor = GridBagConstraints.NORTHWEST; - - fixSyntaxArea(); - - jwtHeaderArea = rSyntaxTextAreaFactory.rSyntaxTextArea(3, 20); - jwtHeaderArea.setMarginLinePosition(70); - jwtHeaderArea.setWhitespaceVisible(true); - SyntaxScheme scheme = jwtHeaderArea.getSyntaxScheme(); - Style style = new Style(); - style.foreground = new Color(222, 133, 10); - scheme.setStyle(Token.LITERAL_STRING_DOUBLE_QUOTE, style); - jwtHeaderArea.revalidate(); - jwtHeaderArea.setHighlightCurrentLine(false); - jwtHeaderArea.setCurrentLineHighlightColor(Color.WHITE); - jwtHeaderArea.setSyntaxEditingStyle(SyntaxConstants.SYNTAX_STYLE_JAVASCRIPT); - jwtHeaderArea.setEditable(true); - jwtHeaderArea.setPopupMenu(new JPopupMenu()); - RTextScrollPane headerPane = new RTextScrollPane(jwtHeaderArea); - headerPane.setVerticalScrollBarPolicy(ScrollPaneConstants.VERTICAL_SCROLLBAR_AS_NEEDED); - headerPane.setLineNumbersEnabled(false); - - - jwtPayloadArea = rSyntaxTextAreaFactory.rSyntaxTextArea(3, 20); - jwtPayloadArea.setMarginLinePosition(70); - jwtPayloadArea.setWhitespaceVisible(true); - scheme = jwtPayloadArea.getSyntaxScheme(); - style = new Style(); - style.foreground = new Color(222, 133, 10); - scheme.setStyle(TokenTypes.LITERAL_STRING_DOUBLE_QUOTE, style); - jwtPayloadArea.revalidate(); - jwtPayloadArea.setHighlightCurrentLine(false); - jwtPayloadArea.setCurrentLineHighlightColor(Color.WHITE); - jwtPayloadArea.setSyntaxEditingStyle(SyntaxConstants.SYNTAX_STYLE_JAVASCRIPT); - jwtPayloadArea.setEditable(true); - jwtPayloadArea.setPopupMenu(new JPopupMenu()); - RTextScrollPane payloadPane = new RTextScrollPane(jwtPayloadArea); - payloadPane.setVerticalScrollBarPolicy(ScrollPaneConstants.VERTICAL_SCROLLBAR_AS_NEEDED); - payloadPane.setLineNumbersEnabled(false); - - jwtSignatureArea = rSyntaxTextAreaFactory.rSyntaxTextArea(3, 10); - jwtSignatureArea.setMarginLinePosition(70); - jwtSignatureArea.setLineWrap(true); - jwtSignatureArea.setWhitespaceVisible(true); - scheme = jwtSignatureArea.getSyntaxScheme(); - style = new Style(); - style.foreground = new Color(222, 133, 10); - scheme.setStyle(TokenTypes.LITERAL_STRING_DOUBLE_QUOTE, style); - jwtSignatureArea.revalidate(); - jwtSignatureArea.setHighlightCurrentLine(false); - jwtSignatureArea.setCurrentLineHighlightColor(Color.WHITE); - jwtSignatureArea.setSyntaxEditingStyle(SyntaxConstants.SYNTAX_STYLE_NONE); - jwtSignatureArea.setEditable(true); - jwtSignatureArea.setPopupMenu(new JPopupMenu()); - RTextScrollPane signaturePane = new RTextScrollPane(jwtSignatureArea); - signaturePane.setVerticalScrollBarPolicy(ScrollPaneConstants.VERTICAL_SCROLLBAR_AS_NEEDED); - signaturePane.setLineNumbersEnabled(false); - - areasPanel.add(headerPane); - areasPanel.add(payloadPane); - areasPanel.add(signaturePane); - - - rdbtnDontModifySignature = new JRadioButton(Strings.dontModify); - rdbtnDontModifySignature.setToolTipText(Strings.dontModifyToolTip); - rdbtnDontModifySignature.setSelected(true); - rdbtnDontModifySignature.setHorizontalAlignment(SwingConstants.LEFT); - c.gridy = 1; - actionPanel.add(rdbtnDontModifySignature, c); - - rdbtnRecalculateSignature = new JRadioButton(Strings.recalculateSignature); - rdbtnRecalculateSignature.putClientProperty(HTML_DISABLE, null); - rdbtnRecalculateSignature.setToolTipText(Strings.recalculateSignatureToolTip); - rdbtnRecalculateSignature.setHorizontalAlignment(SwingConstants.LEFT); - c.gridy = 2; - actionPanel.add(rdbtnRecalculateSignature, c); - - rdbtnOriginalSignature = new JRadioButton(Strings.keepOriginalSignature); - rdbtnOriginalSignature.setToolTipText(Strings.keepOriginalSignatureToolTip); - rdbtnOriginalSignature.setHorizontalAlignment(SwingConstants.LEFT); - c.gridy = 3; - actionPanel.add(rdbtnOriginalSignature, c); - - rdbtnRandomKey = new JRadioButton(Strings.randomKey); - rdbtnRandomKey.putClientProperty(HTML_DISABLE, null); - rdbtnRandomKey.setToolTipText(Strings.randomKeyToolTip); - rdbtnRandomKey.setHorizontalAlignment(SwingConstants.LEFT); - c.gridy = 4; - actionPanel.add(rdbtnRandomKey, c); - - rdbtnChooseSignature = new JRadioButton(Strings.chooseSignature); - rdbtnChooseSignature.setToolTipText(Strings.chooseSignatureToolTip); - rdbtnChooseSignature.setHorizontalAlignment(SwingConstants.LEFT); - c.gridy = 5; - actionPanel.add(rdbtnChooseSignature, c); - - lblSecretKey = new JLabel(Strings.interceptRecalculationKey); - c.gridy = 6; - actionPanel.add(lblSecretKey, c); - - jwtKeyArea = new JTextArea(""); - jwtKeyArea.setRows(3); - jwtKeyArea.setLineWrap(false); - jwtKeyArea.setEnabled(false); - jwtKeyArea.setPreferredSize(new Dimension(400, 55)); - - JScrollPane jp = new JScrollPane(jwtKeyArea); - c.gridy = 7; - c.weightx = 0.9; - actionPanel.add(jp, c); - - lblProblem = new JLabel(""); - lblProblem.setForeground(Color.RED); - c.gridy = 8; - actionPanel.add(lblProblem, c); - - lblNewLabel = new JLabel("Alg None Attack:"); - c.gridy = 9; - actionPanel.add(lblNewLabel, c); - - noneAttackComboBox = new JComboBox<>(); - noneAttackComboBox.setMaximumSize(new Dimension(300, 20)); - noneAttackComboBox.setPreferredSize(new Dimension(300, 20)); - c.gridy = 10; - actionPanel.add(noneAttackComboBox, c); - - chkbxCVEAttack = new JCheckBox("CVE-2018-0114 Attack"); - chkbxCVEAttack.setToolTipText("The public and private key used can be found in src/app/helpers/Strings.java"); - chkbxCVEAttack.setHorizontalAlignment(SwingConstants.LEFT); - c.gridy = 11; - actionPanel.add(chkbxCVEAttack, c); - - lblCookieFlags = new JLabel(""); - lblCookieFlags.putClientProperty(HTML_DISABLE, null); - c.gridy = 12; - actionPanel.add(lblCookieFlags, c); - - lbRegisteredClaims = new JLabel(); - lbRegisteredClaims.putClientProperty(HTML_DISABLE, null); - lbRegisteredClaims.setBackground(SystemColor.controlHighlight); - c.gridy = 13; - actionPanel.add(lbRegisteredClaims, c); - - btnCopyPubPrivKeyCVEAttack = new JButton("Copy used public & private\r\nkey to clipboard used in CVE attack"); - btnCopyPubPrivKeyCVEAttack.setVisible(false); - c.gridy = 14; - actionPanel.add(btnCopyPubPrivKeyCVEAttack, c); - - add(areasPanel); - add(actionPanel); - - setVisible(true); - - btgrp = new ButtonGroup(); - btgrp.add(rdbtnDontModifySignature); - btgrp.add(rdbtnOriginalSignature); - btgrp.add(rdbtnRandomKey); - btgrp.add(rdbtnRecalculateSignature); - btgrp.add(rdbtnChooseSignature); - - - btnCopyPubPrivKeyCVEAttack.addActionListener(a -> Toolkit.getDefaultToolkit() - .getSystemClipboard() - .setContents(new StringSelection("Public Key:\r\n" + Config.cveAttackModePublicKey + "\r\n\r\nPrivate Key:\r\n" - + Config.cveAttackModePrivateKey), null)); - - noneAttackComboBox.addItem(" -"); - noneAttackComboBox.addItem("Alg: none"); - noneAttackComboBox.addItem("Alg: None"); - noneAttackComboBox.addItem("Alg: nOnE"); - noneAttackComboBox.addItem("Alg: NONE"); - - } - - private void fixSyntaxArea() { - JTextComponent.removeKeymap("RTextAreaKeymap"); - UIManager.put("RSyntaxTextAreaUI.actionMap", null); - UIManager.put("RSyntaxTextAreaUI.inputMap", null); - UIManager.put("RTextAreaUI.actionMap", null); - UIManager.put("RTextAreaUI.inputMap", null); - } - - public void setProblemLbl(String txt) { - lblProblem.setText(txt); - } - - public void setRadiosState(boolean enabled) { - Enumeration buttons = btgrp.getElements(); - while (buttons.hasMoreElements()) { - buttons.nextElement().setEnabled(enabled); - } - } - - public AbstractButton getRdbtnDontModify() { - return rdbtnDontModifySignature; - } - - public JRadioButton getRdbtnChooseSignature() { - return rdbtnChooseSignature; - } - - public JRadioButton getRdbtnRecalculateSignature() { - return rdbtnRecalculateSignature; - } - - public JComboBox getNoneAttackComboBox() { - return noneAttackComboBox; - } - - public JCheckBox getCVEAttackCheckBox() { - return chkbxCVEAttack; - } - - public JRadioButton getRdbtnRandomKey() { - return rdbtnRandomKey; - } - - public JButton getCVECopyBtn() { - return btnCopyPubPrivKeyCVEAttack; - } - - public JRadioButton getRdbtnOriginalSignature() { - return rdbtnOriginalSignature; - } - - public void updateSetView(final boolean reset) { - updateSetView(reset, false); - } - - public void updateSetView(final boolean reset, final boolean noKeyUpdate) { - SwingUtilities.invokeLater(() -> { - Output.output("Updating view - reset: " + reset); - - if (reset) { - rdbtnDontModifySignature.setSelected(true); - jwtKeyArea.setText(""); - jwtKeyArea.setEnabled(false); - } else { - jwtHeaderArea.setText(ReadableTokenFormat.jsonBeautify(jwtIM.getJwToken().getHeaderJson())); - jwtPayloadArea.setText(ReadableTokenFormat.jsonBeautify(jwtIM.getJwToken().getPayloadJson())); - jwtSignatureArea.setText(jwtIM.getJwToken().getSignature()); - if (noKeyUpdate) { - jwtKeyArea.setText(jwtIM.getJWTKey()); - } - } - lblProblem.setText(jwtIM.getProblemDetail()); - // -> response of https://oz-web.com/jwt/request_cookie.php - if (jwtIM.getcFW().isCookie()) { - lblCookieFlags.setText(jwtIM.getcFW().toHTMLString()); - } else { - lblCookieFlags.setText(""); - } - lbRegisteredClaims.setText(jwtIM.getTimeClaimsAsText()); - }); - } - - public RSyntaxTextArea getJwtHeaderArea() { - return jwtHeaderArea; - } - - public RSyntaxTextArea getJwtPayloadArea() { - return jwtPayloadArea; - } - - public RSyntaxTextArea getJwtSignatureArea() { - return jwtSignatureArea; - } - - public void setKeyFieldState(boolean state) { - jwtKeyArea.setEnabled(state); - } - - public String getSelectedData() { - return jwtPayloadArea.getSelectedText(); - } - - public String getKeyFieldValue() { - return jwtKeyArea.getText(); - } - - public JTextArea getKeyField() { - return jwtKeyArea; - } - - public void setKeyFieldValue(String string) { - jwtKeyArea.setText(string); - } - -} diff --git a/src/gui/JWTSuiteTab.java b/src/gui/JWTSuiteTab.java deleted file mode 100644 index 9e7dcdb..0000000 --- a/src/gui/JWTSuiteTab.java +++ /dev/null @@ -1,269 +0,0 @@ -package gui; - -import java.awt.Color; -import java.awt.Desktop; -import java.awt.Dimension; -import java.awt.Font; -import java.awt.GridBagConstraints; -import java.awt.GridBagLayout; -import java.awt.Insets; -import java.io.File; -import java.io.IOException; - -import javax.swing.JButton; -import javax.swing.JLabel; -import javax.swing.JPanel; -import javax.swing.JPopupMenu; -import javax.swing.JTextArea; -import javax.swing.SwingUtilities; -import javax.swing.UIManager; -import javax.swing.event.DocumentListener; -import javax.swing.text.JTextComponent; - -import org.fife.ui.rsyntaxtextarea.RSyntaxTextArea; -import org.fife.ui.rsyntaxtextarea.Style; -import org.fife.ui.rsyntaxtextarea.SyntaxConstants; -import org.fife.ui.rsyntaxtextarea.SyntaxScheme; -import org.fife.ui.rsyntaxtextarea.Token; -import org.fife.ui.rtextarea.RTextScrollPane; - -import app.helpers.Config; -import model.JWTSuiteTabModel; -import model.Strings; - -public class JWTSuiteTab extends JPanel { - - private static final long serialVersionUID = 1L; - public static final String TAHOMA = "Tahoma"; - private JTextArea jwtInputField; - private RSyntaxTextArea jwtOuputField; - private JButton jwtSignatureButton; - private JTextArea jwtKeyArea; - private final JWTSuiteTabModel jwtSTM; - private final RSyntaxTextAreaFactory rSyntaxTextAreaFactory; - private JLabel lbRegisteredClaims; - private JLabel lblExtendedVerificationInfo; - - public JWTSuiteTab(JWTSuiteTabModel jwtSTM, RSyntaxTextAreaFactory rSyntaxTextAreaFactory) { - this.rSyntaxTextAreaFactory = rSyntaxTextAreaFactory; - drawGui(); - this.jwtSTM = jwtSTM; - } - - - public void updateSetView() { - SwingUtilities.invokeLater(() -> { - if (!jwtInputField.getText().equals(jwtSTM.getJwtInput())) { - jwtInputField.setText(jwtSTM.getJwtInput()); - } - if (!jwtSignatureButton.getText().equals(jwtSTM.getVerificationLabel())) { - jwtSignatureButton.setText(jwtSTM.getVerificationLabel()); - } - if (!jwtOuputField.getText().equals(jwtSTM.getJwtJSON())) { - jwtOuputField.setText(jwtSTM.getJwtJSON()); - } - if (!jwtKeyArea.getText().equals(jwtSTM.getJwtKey())) { - jwtKeyArea.setText(jwtSTM.getJwtKey()); - } - if (!jwtSignatureButton.getBackground().equals(jwtSTM.getJwtSignatureColor())) { - jwtSignatureButton.setBackground(jwtSTM.getJwtSignatureColor()); - } - if (jwtKeyArea.getText().equals("")) { - jwtSTM.setJwtSignatureColor(new JButton().getBackground()); - jwtSignatureButton.setBackground(jwtSTM.getJwtSignatureColor()); - } - lblExtendedVerificationInfo.setText(jwtSTM.getVerificationResult()); - lbRegisteredClaims.setText(jwtSTM.getTimeClaimsAsText()); - jwtOuputField.setCaretPosition(0); - }); - } - - public void registerDocumentListener(DocumentListener jwtInputListener, DocumentListener jwtKeyListener) { - jwtInputField.getDocument().addDocumentListener(jwtInputListener); - jwtKeyArea.getDocument().addDocumentListener(jwtKeyListener); - } - - private void drawGui() { - GridBagLayout gridBagLayout = new GridBagLayout(); - gridBagLayout.columnWidths = new int[]{10, 0, 0, 0}; - gridBagLayout.rowHeights = new int[]{10, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}; - gridBagLayout.columnWeights = new double[]{0.0, 1.0, 0.0, Double.MIN_VALUE}; - gridBagLayout.rowWeights = new double[]{0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, - Double.MIN_VALUE}; - setLayout(gridBagLayout); - - JLabel lblPasteJwtToken = new JLabel(Strings.enterJWT); - lblPasteJwtToken.setFont(new Font(TAHOMA, Font.BOLD, 12)); - GridBagConstraints gbc_lblPasteJwtToken = new GridBagConstraints(); - gbc_lblPasteJwtToken.anchor = GridBagConstraints.SOUTHWEST; - gbc_lblPasteJwtToken.insets = new Insets(0, 0, 5, 5); - gbc_lblPasteJwtToken.gridx = 1; - gbc_lblPasteJwtToken.gridy = 1; - add(lblPasteJwtToken, gbc_lblPasteJwtToken); - - JButton creditButton = new JButton("About"); - creditButton.addActionListener(arg0 -> { - JLabelLink jLabelLink = new JLabelLink(Strings.creditTitle, 530, 565); - - jLabelLink.addText( - "

About JWT4B

JSON Web Tokens (also known as JWT4B) is developed by Oussama Zgheb and Matthias Vetsch.
"); - jLabelLink.addURL("Mantainer Website", "zgheb.com"); - jLabelLink.addURL("GitHub Repository", "github.com/mvetsch/JWT4B"); - jLabelLink.addText("

"); - jLabelLink.addText( - "JWT4B, excluding the libraries mentioned below and the Burp extender classes, uses the GPL 3 license."); - jLabelLink.addURL( - "* RSyntaxTextArea", - "github.com/bobbylight/RSyntaxTextArea"); - jLabelLink.addURL("* Auth0 -java-jwt", - "github.com/auth0/java-jwt"); - jLabelLink.addURL("* Apache Commons Lang", "apache.org"); - jLabelLink.addText("

"); - jLabelLink.addText("Thanks to Compass Security AG for providing development time for the initial version
"); - jLabelLink.addURL("compass-security.com
", - "compass-security.com"); - jLabelLink.addText("and to Brainloop for providing broader token support!"); - jLabelLink.addURL("brainloop.com

", "brainloop.com"); - - jLabelLink.addLogoImage(); - }); - GridBagConstraints gbc_creditButton = new GridBagConstraints(); - gbc_creditButton.insets = new Insets(0, 0, 5, 0); - gbc_creditButton.gridx = 2; - gbc_creditButton.gridy = 1; - gbc_creditButton.fill = GridBagConstraints.HORIZONTAL; - add(creditButton, gbc_creditButton); - - - JButton configButton = new JButton("Change Config"); - configButton.addActionListener(arg0 -> { - File file = new File(Config.configPath); - Desktop desktop = Desktop.getDesktop(); - try { - desktop.open(file); - } catch (IOException e) { - System.err.println("Error using Desktop API - " + e.getMessage() + " - " + e.getCause()); - } - }); - - GridBagConstraints gbc_configButton = new GridBagConstraints(); - gbc_configButton.insets = new Insets(0, 0, 5, 0); - gbc_configButton.gridx = 2; - gbc_configButton.gridy = 2; - gbc_configButton.fill = GridBagConstraints.HORIZONTAL; - add(configButton, gbc_configButton); - - jwtInputField = new JTextArea(); - jwtInputField.setBorder(UIManager.getLookAndFeel().getDefaults().getBorder("TextField.border")); - jwtInputField.setRows(2); - jwtInputField.setLineWrap(true); - jwtInputField.setWrapStyleWord(true); - - GridBagConstraints gbc_jwtInputField = new GridBagConstraints(); - gbc_jwtInputField.insets = new Insets(0, 0, 5, 5); - gbc_jwtInputField.fill = GridBagConstraints.BOTH; - gbc_jwtInputField.gridx = 1; - gbc_jwtInputField.gridy = 2; - add(jwtInputField, gbc_jwtInputField); - - JLabel lblEnterSecret = new JLabel(Strings.enterSecretKey); - lblEnterSecret.setFont(new Font(TAHOMA, Font.BOLD, 12)); - GridBagConstraints gbc_lblEnterSecret = new GridBagConstraints(); - gbc_lblEnterSecret.anchor = GridBagConstraints.WEST; - gbc_lblEnterSecret.insets = new Insets(0, 0, 5, 5); - gbc_lblEnterSecret.gridx = 1; - gbc_lblEnterSecret.gridy = 3; - add(lblEnterSecret, gbc_lblEnterSecret); - - jwtKeyArea = new JTextArea(); - jwtKeyArea.setBorder(UIManager.getLookAndFeel().getDefaults().getBorder("TextField.border")); - GridBagConstraints gbc_jwtKeyField = new GridBagConstraints(); - gbc_jwtKeyField.insets = new Insets(0, 0, 5, 5); - gbc_jwtKeyField.fill = GridBagConstraints.HORIZONTAL; - gbc_jwtKeyField.gridx = 1; - gbc_jwtKeyField.gridy = 4; - add(jwtKeyArea, gbc_jwtKeyField); - jwtKeyArea.setColumns(10); - - jwtSignatureButton = new JButton(""); - Dimension preferredSize = new Dimension(400, 30); - jwtSignatureButton.setPreferredSize(preferredSize); - - GridBagConstraints gbc_jwtSignatureButton = new GridBagConstraints(); - gbc_jwtSignatureButton.insets = new Insets(0, 0, 5, 5); - gbc_jwtSignatureButton.gridx = 1; - gbc_jwtSignatureButton.gridy = 6; - add(jwtSignatureButton, gbc_jwtSignatureButton); - - GridBagConstraints gbc_jwtOuputField = new GridBagConstraints(); - gbc_jwtOuputField.insets = new Insets(0, 0, 5, 5); - gbc_jwtOuputField.fill = GridBagConstraints.BOTH; - gbc_jwtOuputField.gridx = 1; - gbc_jwtOuputField.gridy = 9; - - JTextComponent.removeKeymap("RTextAreaKeymap"); - jwtOuputField = rSyntaxTextAreaFactory.rSyntaxTextArea(); - UIManager.put("RSyntaxTextAreaUI.actionMap", null); - UIManager.put("RSyntaxTextAreaUI.inputMap", null); - UIManager.put("RTextAreaUI.actionMap", null); - UIManager.put("RTextAreaUI.inputMap", null); - jwtOuputField.setWhitespaceVisible(true); - - SyntaxScheme scheme = jwtOuputField.getSyntaxScheme(); - Style style = new Style(); - style.foreground = new Color(222, 133, 10); - scheme.setStyle(Token.LITERAL_STRING_DOUBLE_QUOTE, style); - jwtOuputField.revalidate(); - jwtOuputField.setHighlightCurrentLine(false); - jwtOuputField.setCurrentLineHighlightColor(Color.WHITE); - jwtOuputField.setSyntaxEditingStyle(SyntaxConstants.SYNTAX_STYLE_JAVASCRIPT); - jwtOuputField.setEditable(false); - // no context menu on right-click - jwtOuputField.setPopupMenu(new JPopupMenu()); - - // hopefully fixing: - // java.lang.ClassCastException: class javax.swing.plaf.nimbus.DerivedColor$UIResource cannot be cast to class - // java.lang.Boolean (javax.swing.plaf.nimbus.DerivedColor$UIResource is in module java.desktop of loader 'bootstrap'; - // java.lang.Boolean is in module java.base of loader 'bootstrap') - SwingUtilities.invokeLater(() -> { - RTextScrollPane sp = new RTextScrollPane(jwtOuputField); - sp.setLineNumbersEnabled(false); - add(sp, gbc_jwtOuputField); - }); - - lblExtendedVerificationInfo = new JLabel(""); - GridBagConstraints gbc_lblExtendedVerificationInfo = new GridBagConstraints(); - gbc_lblExtendedVerificationInfo.insets = new Insets(0, 0, 5, 5); - gbc_lblExtendedVerificationInfo.gridx = 1; - gbc_lblExtendedVerificationInfo.gridy = 7; - add(lblExtendedVerificationInfo, gbc_lblExtendedVerificationInfo); - - JLabel lblDecodedJwt = new JLabel(Strings.decodedJWT); - lblDecodedJwt.setFont(new Font(TAHOMA, Font.BOLD, 12)); - GridBagConstraints gbc_lblDecodedJwt = new GridBagConstraints(); - gbc_lblDecodedJwt.anchor = GridBagConstraints.WEST; - gbc_lblDecodedJwt.insets = new Insets(0, 0, 5, 5); - gbc_lblDecodedJwt.gridx = 1; - gbc_lblDecodedJwt.gridy = 8; - add(lblDecodedJwt, gbc_lblDecodedJwt); - - - lbRegisteredClaims = new JLabel(); - lbRegisteredClaims.putClientProperty("html.disable", false); - lbRegisteredClaims.setBackground(new Color(238, 238, 238)); - GridBagConstraints gbc_lbRegisteredClaims = new GridBagConstraints(); - gbc_lbRegisteredClaims.fill = GridBagConstraints.BOTH; - gbc_lbRegisteredClaims.insets = new Insets(0, 0, 5, 5); - gbc_lbRegisteredClaims.gridx = 1; - gbc_lbRegisteredClaims.gridy = 11; - add(lbRegisteredClaims, gbc_lbRegisteredClaims); - } - - public String getJWTInput() { - return jwtInputField.getText(); - } - - public String getKeyInput() { - return jwtKeyArea.getText(); - } -} diff --git a/src/gui/JWTViewTab.java b/src/gui/JWTViewTab.java deleted file mode 100644 index eda4059..0000000 --- a/src/gui/JWTViewTab.java +++ /dev/null @@ -1,238 +0,0 @@ -package gui; - -import java.awt.Color; -import java.awt.Dimension; -import java.awt.Font; -import java.awt.GridBagConstraints; -import java.awt.GridBagLayout; -import java.awt.Insets; -import java.awt.SystemColor; -import java.awt.event.ActionEvent; -import java.awt.event.ActionListener; - -import javax.swing.JButton; -import javax.swing.JLabel; -import javax.swing.JPanel; -import javax.swing.JPopupMenu; -import javax.swing.JTextArea; -import javax.swing.SwingUtilities; -import javax.swing.UIManager; -import javax.swing.event.DocumentListener; -import javax.swing.text.JTextComponent; - -import org.fife.ui.rsyntaxtextarea.RSyntaxTextArea; -import org.fife.ui.rsyntaxtextarea.Style; -import org.fife.ui.rsyntaxtextarea.SyntaxConstants; -import org.fife.ui.rsyntaxtextarea.SyntaxScheme; -import org.fife.ui.rsyntaxtextarea.Token; -import org.fife.ui.rtextarea.RTextScrollPane; - -import app.algorithm.AlgorithmType; -import model.JWTTabModel; -import model.Strings; - -public class JWTViewTab extends JPanel { - - private static final long serialVersionUID = 1L; - private RSyntaxTextArea outputField; - private JTextArea jwtKeyArea; - private JLabel keyLabel; - private JButton verificationIndicator; - private final JWTTabModel jwtTM; - private final RSyntaxTextAreaFactory rSyntaxTextAreaFactory; - private JLabel lblCookieFlags; - private JLabel lbRegisteredClaims; - - public JWTViewTab(JWTTabModel jwtTM, RSyntaxTextAreaFactory rSyntaxTextAreaFactory) { - this.rSyntaxTextAreaFactory = rSyntaxTextAreaFactory; - drawPanel(); - this.jwtTM = jwtTM; - } - - public void registerDocumentListener(DocumentListener inputFieldListener) { - jwtKeyArea.getDocument().addDocumentListener(inputFieldListener); - } - - - private void drawPanel() { - GridBagLayout gridBagLayout = new GridBagLayout(); - gridBagLayout.columnWidths = new int[]{10, 447, 0, 0}; - gridBagLayout.rowHeights = new int[]{10, 10, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}; - gridBagLayout.columnWeights = new double[]{0.0, 1.0, 0.0, Double.MIN_VALUE}; - gridBagLayout.rowWeights = new double[]{0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, Double.MIN_VALUE}; - setLayout(gridBagLayout); - - keyLabel = new JLabel(" "); - keyLabel.setFont(new Font("Tahoma", Font.BOLD, 12)); - GridBagConstraints gbc_inputLabel1 = new GridBagConstraints(); - gbc_inputLabel1.fill = GridBagConstraints.VERTICAL; - gbc_inputLabel1.insets = new Insets(0, 0, 5, 5); - gbc_inputLabel1.anchor = GridBagConstraints.WEST; - gbc_inputLabel1.gridx = 1; - gbc_inputLabel1.gridy = 1; - add(keyLabel, gbc_inputLabel1); - - jwtKeyArea = new JTextArea(); - jwtKeyArea.setBorder(UIManager.getLookAndFeel().getDefaults().getBorder("TextField.border")); - GridBagConstraints gbc_inputField1 = new GridBagConstraints(); - gbc_inputField1.insets = new Insets(0, 0, 5, 5); - gbc_inputField1.fill = GridBagConstraints.HORIZONTAL; - gbc_inputField1.gridx = 1; - gbc_inputField1.gridy = 2; - add(jwtKeyArea, gbc_inputField1); - jwtKeyArea.setColumns(10); - - verificationIndicator = new JButton(""); - verificationIndicator.setText(Strings.noSecretsProvided); - verificationIndicator.addActionListener(new ActionListener() { - - public void actionPerformed(ActionEvent e) { - } - }); - Dimension preferredSize = new Dimension(400, 30); - verificationIndicator.setPreferredSize(preferredSize); - GridBagConstraints gbc_validIndicator = new GridBagConstraints(); - gbc_validIndicator.insets = new Insets(0, 0, 5, 5); - gbc_validIndicator.gridx = 1; - gbc_validIndicator.gridy = 4; - add(verificationIndicator, gbc_validIndicator); - - JTextComponent.removeKeymap("RTextAreaKeymap"); - outputField = rSyntaxTextAreaFactory.rSyntaxTextArea(); - UIManager.put("RSyntaxTextAreaUI.actionMap", null); - UIManager.put("RSyntaxTextAreaUI.inputMap", null); - UIManager.put("RTextAreaUI.actionMap", null); - UIManager.put("RTextAreaUI.inputMap", null); - - outputField.setWhitespaceVisible(true); - SyntaxScheme scheme = outputField.getSyntaxScheme(); - Style style = new Style(); - style.foreground = new Color(222, 133, 10); - scheme.setStyle(Token.LITERAL_STRING_DOUBLE_QUOTE, style); - outputField.revalidate(); - outputField.setHighlightCurrentLine(false); - outputField.setCurrentLineHighlightColor(Color.WHITE); - outputField.setSyntaxEditingStyle(SyntaxConstants.SYNTAX_STYLE_JAVASCRIPT); - outputField.setEditable(false); - outputField.setPopupMenu(new JPopupMenu()); // no context menu on right-click - - JLabel outputLabel = new JLabel("JWT"); - outputLabel.setFont(new Font("Tahoma", Font.BOLD, 12)); - GridBagConstraints gbc_outputLabel = new GridBagConstraints(); - gbc_outputLabel.anchor = GridBagConstraints.WEST; - gbc_outputLabel.insets = new Insets(0, 0, 5, 5); - gbc_outputLabel.gridx = 1; - gbc_outputLabel.gridy = 5; - add(outputLabel, gbc_outputLabel); - - lbRegisteredClaims = new JLabel(); - lbRegisteredClaims.putClientProperty("html.disable", null); - lbRegisteredClaims.setBackground(SystemColor.controlHighlight); - GridBagConstraints gbc_lbRegisteredClaims = new GridBagConstraints(); - gbc_lbRegisteredClaims.fill = GridBagConstraints.BOTH; - gbc_lbRegisteredClaims.insets = new Insets(0, 0, 5, 5); - gbc_lbRegisteredClaims.gridx = 1; - gbc_lbRegisteredClaims.gridy = 8; - add(lbRegisteredClaims, gbc_lbRegisteredClaims); - - lblCookieFlags = new JLabel(" "); - lblCookieFlags.putClientProperty("html.disable", null); - lblCookieFlags.setFont(new Font("Tahoma", Font.BOLD, 12)); - GridBagConstraints gbc_lblCookieFlags = new GridBagConstraints(); - gbc_lblCookieFlags.anchor = GridBagConstraints.SOUTHWEST; - gbc_lblCookieFlags.insets = new Insets(0, 0, 5, 5); - gbc_lblCookieFlags.gridx = 1; - gbc_lblCookieFlags.gridy = 9; - add(lblCookieFlags, gbc_lblCookieFlags); - - RTextScrollPane sp = new RTextScrollPane(outputField); - sp.setLineNumbersEnabled(false); - - GridBagConstraints gbc_outputfield = new GridBagConstraints(); - gbc_outputfield.insets = new Insets(0, 0, 5, 5); - gbc_outputfield.fill = GridBagConstraints.BOTH; - gbc_outputfield.gridx = 1; - gbc_outputfield.gridy = 6; - add(sp, gbc_outputfield); - - } - - public JTextArea getOutputfield() { - return outputField; - } - - public String getKeyValue() { - return jwtKeyArea.getText(); - } - - public void setKeyValue(String value) { - jwtKeyArea.setText(value); - } - - public void setVerificationResult(String value) { - verificationIndicator.setText(value); - } - - public void setVerificationResultColor(Color verificationResultColor) { - verificationIndicator.setBackground(verificationResultColor); - } - - public void setCaret() { - outputField.setCaretPosition(0); - } - - public String getSelectedData() { - return getOutputfield().getSelectedText(); - } - - - public void updateSetView(AlgorithmType algorithmType) { - SwingUtilities.invokeLater(new Runnable() { - - public void run() { - if (!jwtTM.getJWTJSON().equals(outputField.getText())) { - outputField.setText(jwtTM.getJWTJSON()); - } - if (!jwtTM.getKeyLabel().equals(keyLabel.getText())) { - keyLabel.setText(jwtTM.getKeyLabel()); - } - if (!jwtTM.getKey().equals(jwtKeyArea.getText())) { - jwtKeyArea.setText(jwtTM.getKey()); - } - if (!jwtTM.getVerificationColor().equals(verificationIndicator.getBackground())) { - verificationIndicator.setBackground(jwtTM.getVerificationColor()); - } - if (!jwtTM.getVerificationLabel().equals(verificationIndicator.getText())) { - if (jwtTM.getVerificationLabel().equals("")) { - verificationIndicator.setText(Strings.noSecretsProvided); - } else { - verificationIndicator.setText(jwtTM.getVerificationLabel()); - } - - } - if (algorithmType.equals(AlgorithmType.SYMMETRIC)) { - keyLabel.setText("Secret"); - jwtKeyArea.setEnabled(true); - } - if (algorithmType.equals(AlgorithmType.ASYMMETRIC)) { - keyLabel.setText("Public Key"); - jwtKeyArea.setEnabled(true); - } - if (algorithmType.equals(AlgorithmType.NONE)) { - keyLabel.setText(""); - jwtKeyArea.setEnabled(false); - jwtKeyArea.setEnabled(false); - } - - if (jwtTM.getcFW().isCookie()) { - lblCookieFlags.setText(jwtTM.getcFW().toHTMLString()); - } else { - lblCookieFlags.setText(""); - } - setCaret(); - lbRegisteredClaims.setText(jwtTM.getTimeClaimsAsText()); - } - }); - } - -} diff --git a/src/gui/RSyntaxTextAreaFactory.java b/src/gui/RSyntaxTextAreaFactory.java deleted file mode 100644 index 131bf46..0000000 --- a/src/gui/RSyntaxTextAreaFactory.java +++ /dev/null @@ -1,69 +0,0 @@ -package gui; - -import static app.helpers.Output.outputError; - -import java.io.IOException; - -import org.fife.ui.rsyntaxtextarea.RSyntaxTextArea; -import org.fife.ui.rsyntaxtextarea.Theme; - -import burp.IBurpExtenderCallbacks; - -public class RSyntaxTextAreaFactory { - - private final ThemeDetector themeDetector; - - public RSyntaxTextAreaFactory(IBurpExtenderCallbacks callbacks) { - this.themeDetector = new ThemeDetector(callbacks); - } - - RSyntaxTextArea rSyntaxTextArea() { - return new BurpThemeAwareRSyntaxTextArea(themeDetector); - } - - RSyntaxTextArea rSyntaxTextArea(int rows, int cols) { - return new BurpThemeAwareRSyntaxTextArea(themeDetector, rows, cols); - } - - private static class BurpThemeAwareRSyntaxTextArea extends RSyntaxTextArea { - - private static final String THEME_PATH = "/org/fife/ui/rsyntaxtextarea/themes/"; - private static final String DARK_THEME = THEME_PATH + "dark.xml"; - private static final String LIGHT_THEME = THEME_PATH + "default.xml"; - - private final ThemeDetector themeDetector; - - private BurpThemeAwareRSyntaxTextArea(ThemeDetector themeDetector) { - this.themeDetector = themeDetector; - applyTheme(); - } - - public BurpThemeAwareRSyntaxTextArea(ThemeDetector themeDetector, int rows, int cols) { - super(rows, cols); - this.themeDetector = themeDetector; - applyTheme(); - } - - @Override - public void updateUI() { - super.updateUI(); - - if (themeDetector != null) { - applyTheme(); - } - } - - private void applyTheme() { - String themeResource = themeDetector.isLightTheme() ? LIGHT_THEME : DARK_THEME; - - try { - Theme theme = Theme.load(getClass().getResourceAsStream(themeResource)); - theme.apply(this); - } catch (IOException e) { - outputError("Unable to apply rsyntax theme: " + e.getMessage()); - } - } - } - - -} diff --git a/src/gui/ThemeDetector.java b/src/gui/ThemeDetector.java deleted file mode 100644 index b846726..0000000 --- a/src/gui/ThemeDetector.java +++ /dev/null @@ -1,25 +0,0 @@ -package gui; - -import static java.awt.Color.WHITE; - -import javax.swing.JLabel; - -import burp.IBurpExtenderCallbacks; -import model.Settings; - -public class ThemeDetector { - - private final IBurpExtenderCallbacks callbacks; - - ThemeDetector(IBurpExtenderCallbacks callbacks) { - this.callbacks = callbacks; - } - - boolean isLightTheme() { - JLabel label = new JLabel(); - callbacks.customizeUiComponent(label); - boolean isLight = label.getBackground().equals(WHITE); - Settings.isLight = isLight; // TODO not so nice, since settings are static - return isLight; - } -} diff --git a/src/app/algorithm/AlgorithmType.java b/src/main/java/app/algorithm/AlgorithmType.java similarity index 60% rename from src/app/algorithm/AlgorithmType.java rename to src/main/java/app/algorithm/AlgorithmType.java index 574535b..a672488 100644 --- a/src/app/algorithm/AlgorithmType.java +++ b/src/main/java/app/algorithm/AlgorithmType.java @@ -1,5 +1,5 @@ -package app.algorithm; - -public enum AlgorithmType { - NONE, SYMMETRIC, ASYMMETRIC -} +package app.algorithm; + +public enum AlgorithmType { + NONE, SYMMETRIC, ASYMMETRIC +} diff --git a/src/app/algorithm/AlgorithmWrapper.java b/src/main/java/app/algorithm/AlgorithmWrapper.java similarity index 93% rename from src/app/algorithm/AlgorithmWrapper.java rename to src/main/java/app/algorithm/AlgorithmWrapper.java index 3fcc62b..ded79b0 100644 --- a/src/app/algorithm/AlgorithmWrapper.java +++ b/src/main/java/app/algorithm/AlgorithmWrapper.java @@ -1,181 +1,188 @@ -package app.algorithm; - -import com.auth0.jwt.algorithms.Algorithm; -import com.auth0.jwt.exceptions.AlgorithmMismatchException; -import com.auth0.jwt.interfaces.ECDSAKeyProvider; -import com.auth0.jwt.interfaces.RSAKeyProvider; - -import java.security.interfaces.ECPrivateKey; -import java.security.interfaces.ECPublicKey; -import java.security.interfaces.RSAPrivateKey; -import java.security.interfaces.RSAPublicKey; -import java.util.stream.Stream; - -import static app.helpers.KeyHelper.getKeyInstance; -import static app.algorithm.AlgorithmType.ASYMMETRIC; -import static app.algorithm.AlgorithmType.SYMMETRIC; - -public enum AlgorithmWrapper { - NONE("none", AlgorithmType.NONE), - HS256("HS256", SYMMETRIC), - HS384("HS384", SYMMETRIC), - HS512("HS512", SYMMETRIC), - RS256("RS256", ASYMMETRIC), - RS384("RS384", ASYMMETRIC), - RS512("RS512", ASYMMETRIC), - ES256("ES256", ASYMMETRIC), - ES256K("ES256K", ASYMMETRIC), - ES384("ES384", ASYMMETRIC), - ES512("ES512", ASYMMETRIC); - - private final String algorithmName; - private final AlgorithmType type; - - AlgorithmWrapper(String algorithmName, AlgorithmType none) { - this.algorithmName = algorithmName; - this.type = none; - } - - String algorithmName() { - return algorithmName; - } - - private Algorithm algorithm(byte[] key) { - switch (this) { - // HMAC with SHA-XXX - case HS256: - return Algorithm.HMAC256(key); - case HS384: - return Algorithm.HMAC384(key); - case HS512: - return Algorithm.HMAC512(key); - - // ECDSA with curve - case ES256: - return Algorithm.ECDSA256(new ECDSAKeyProviderImpl(new String(key))); - case ES256K: - return Algorithm.ECDSA256K(new ECDSAKeyProviderImpl(new String(key))); - case ES384: - return Algorithm.ECDSA384(new ECDSAKeyProviderImpl(new String(key))); - case ES512: - return Algorithm.ECDSA512(new ECDSAKeyProviderImpl(new String(key))); - - // RSASSA-PKCS1-v1_5 with SHA-XXX - case RS256: - return Algorithm.RSA256(new RSAKeyProviderImpl(new String(key))); - case RS384: - return Algorithm.RSA384(new RSAKeyProviderImpl(new String(key))); - case RS512: - return Algorithm.RSA512(new RSAKeyProviderImpl(new String(key))); - - case NONE: - default: - throw new AlgorithmMismatchException("Unsupported algorithm '" + algorithmName + "'"); - } - } - - private Algorithm algorithm(String key) { - switch (this) { - // HMAC with SHA-XXX - case HS256: - return Algorithm.HMAC256(key); - case HS384: - return Algorithm.HMAC384(key); - case HS512: - return Algorithm.HMAC512(key); - - // ECDSA with curve - case ES256: - return Algorithm.ECDSA256(new ECDSAKeyProviderImpl(key)); - case ES256K: - return Algorithm.ECDSA256K(new ECDSAKeyProviderImpl(key)); - case ES384: - return Algorithm.ECDSA384(new ECDSAKeyProviderImpl(key)); - case ES512: - return Algorithm.ECDSA512(new ECDSAKeyProviderImpl(key)); - - // RSASSA-PKCS1-v1_5 with SHA-XXX - case RS256: - return Algorithm.RSA256(new RSAKeyProviderImpl(key)); - case RS384: - return Algorithm.RSA384(new RSAKeyProviderImpl(key)); - case RS512: - return Algorithm.RSA512(new RSAKeyProviderImpl(key)); - - case NONE: - default: - throw new AlgorithmMismatchException("Unsupported algorithm '" + algorithmName + "'"); - } - } - public static Algorithm getVerifierAlgorithm(String algo, String key) { - return withName(algo).algorithm(key); - } - - public static Algorithm getSignerAlgorithm(String algo, String key) { - return withName(algo).algorithm(key); - } - - public static Algorithm getSignerAlgorithm(String algo, byte[] key) { - return withName(algo).algorithm(key); - } - public static AlgorithmWrapper withName(String algorithm) { - return Stream.of(AlgorithmWrapper.values()) - .filter(supportedAlgorithm -> algorithm.equals(supportedAlgorithm.algorithmName())) - .findFirst() - .orElseThrow(() -> new AlgorithmMismatchException("Unsupported algorithm '" + algorithm + "'")); - } - - public static AlgorithmType getTypeOf(String algorithm) { - return Stream.of(AlgorithmWrapper.values()) - .filter(supportedAlgorithm -> algorithm.equals(supportedAlgorithm.algorithmName())) - .map(supportedAlgorithm -> supportedAlgorithm.type) - .findFirst() - .orElse(AlgorithmType.NONE); - } - - private static class RSAKeyProviderImpl implements RSAKeyProvider { - private final String key; - - private RSAKeyProviderImpl(String key) { - this.key = key; - } - - @Override - public RSAPublicKey getPublicKeyById(String kid) { - return (RSAPublicKey) getKeyInstance(key, "RSA", false); - } - - @Override - public RSAPrivateKey getPrivateKey() { - return (RSAPrivateKey) getKeyInstance(key, "RSA", true); - } - - @Override - public String getPrivateKeyId() { - return "id"; - } - } - - private static class ECDSAKeyProviderImpl implements ECDSAKeyProvider { - private final String key; - - private ECDSAKeyProviderImpl(String key) { - this.key = key; - } - - @Override - public ECPublicKey getPublicKeyById(String kid) { - return (ECPublicKey) getKeyInstance(key, "EC", false); - } - - @Override - public ECPrivateKey getPrivateKey() { - return (ECPrivateKey) getKeyInstance(key, "EC", true); - } - - @Override - public String getPrivateKeyId() { - return "id"; - } - } -} +package app.algorithm; + +import com.auth0.jwt.algorithms.Algorithm; +import com.auth0.jwt.exceptions.AlgorithmMismatchException; +import com.auth0.jwt.interfaces.ECDSAKeyProvider; +import com.auth0.jwt.interfaces.RSAKeyProvider; + +import java.security.interfaces.ECPrivateKey; +import java.security.interfaces.ECPublicKey; +import java.security.interfaces.RSAPrivateKey; +import java.security.interfaces.RSAPublicKey; +import java.util.stream.Stream; + +import static app.helpers.KeyHelper.getKeyInstance; +import static app.algorithm.AlgorithmType.ASYMMETRIC; +import static app.algorithm.AlgorithmType.SYMMETRIC; + +public enum AlgorithmWrapper { + NONE("none", AlgorithmType.NONE), + HS256("HS256", SYMMETRIC), + HS384("HS384", SYMMETRIC), + HS512("HS512", SYMMETRIC), + RS256("RS256", ASYMMETRIC), + RS384("RS384", ASYMMETRIC), + RS512("RS512", ASYMMETRIC), + ES256("ES256", ASYMMETRIC), + ES256K("ES256K", ASYMMETRIC), + ES384("ES384", ASYMMETRIC), + ES512("ES512", ASYMMETRIC); + + private final String algorithmName; + private final AlgorithmType type; + + AlgorithmWrapper(String algorithmName, AlgorithmType none) { + this.algorithmName = algorithmName; + this.type = none; + } + + String algorithmName() { + return algorithmName; + } + + private Algorithm algorithm(byte[] key) { + switch (this) { + // HMAC with SHA-XXX + case HS256: + return Algorithm.HMAC256(key); + case HS384: + return Algorithm.HMAC384(key); + case HS512: + return Algorithm.HMAC512(key); + + // ECDSA with curve + case ES256: + return Algorithm.ECDSA256(new ECDSAKeyProviderImpl(new String(key))); + case ES256K: + return Algorithm.ECDSA256K(new ECDSAKeyProviderImpl(new String(key))); + case ES384: + return Algorithm.ECDSA384(new ECDSAKeyProviderImpl(new String(key))); + case ES512: + return Algorithm.ECDSA512(new ECDSAKeyProviderImpl(new String(key))); + + // RSASSA-PKCS1-v1_5 with SHA-XXX + case RS256: + return Algorithm.RSA256(new RSAKeyProviderImpl(new String(key))); + case RS384: + return Algorithm.RSA384(new RSAKeyProviderImpl(new String(key))); + case RS512: + return Algorithm.RSA512(new RSAKeyProviderImpl(new String(key))); + + case NONE: + default: + throwUnsupportedAlgo(); + return null; + } + } + + private Algorithm algorithm(String key) { + switch (this) { + // HMAC with SHA-XXX + case HS256: + return Algorithm.HMAC256(key); + case HS384: + return Algorithm.HMAC384(key); + case HS512: + return Algorithm.HMAC512(key); + + // ECDSA with curve + case ES256: + return Algorithm.ECDSA256(new ECDSAKeyProviderImpl(key)); + case ES256K: + return Algorithm.ECDSA256K(new ECDSAKeyProviderImpl(key)); + case ES384: + return Algorithm.ECDSA384(new ECDSAKeyProviderImpl(key)); + case ES512: + return Algorithm.ECDSA512(new ECDSAKeyProviderImpl(key)); + + // RSASSA-PKCS1-v1_5 with SHA-XXX + case RS256: + return Algorithm.RSA256(new RSAKeyProviderImpl(key)); + case RS384: + return Algorithm.RSA384(new RSAKeyProviderImpl(key)); + case RS512: + return Algorithm.RSA512(new RSAKeyProviderImpl(key)); + + case NONE: + default: + throwUnsupportedAlgo(); + return null; + } + } + public static Algorithm getVerifierAlgorithm(String algo, String key) { + return withName(algo).algorithm(key); + } + + public static Algorithm getSignerAlgorithm(String algo, String key) { + return withName(algo).algorithm(key); + } + + public static Algorithm getSignerAlgorithm(String algo, byte[] key) { + return withName(algo).algorithm(key); + } + public static AlgorithmWrapper withName(String algorithm) { + return Stream.of(AlgorithmWrapper.values()) + .filter(supportedAlgorithm -> algorithm.equals(supportedAlgorithm.algorithmName())) + .findFirst() + .orElseThrow(() -> new AlgorithmMismatchException("Unsupported algorithm '" + algorithm + "'")); + } + + public static AlgorithmType getTypeOf(String algorithm) { + return Stream.of(AlgorithmWrapper.values()) + .filter(supportedAlgorithm -> algorithm.equals(supportedAlgorithm.algorithmName())) + .map(supportedAlgorithm -> supportedAlgorithm.type) + .findFirst() + .orElse(AlgorithmType.NONE); + } + + private static class RSAKeyProviderImpl implements RSAKeyProvider { + private final String key; + + private RSAKeyProviderImpl(String key) { + this.key = key; + } + + @Override + public RSAPublicKey getPublicKeyById(String kid) { + return (RSAPublicKey) getKeyInstance(key, "RSA", false); + } + + @Override + public RSAPrivateKey getPrivateKey() { + return (RSAPrivateKey) getKeyInstance(key, "RSA", true); + } + + @Override + public String getPrivateKeyId() { + return "id"; + } + } + + private static class ECDSAKeyProviderImpl implements ECDSAKeyProvider { + private final String key; + + private ECDSAKeyProviderImpl(String key) { + this.key = key; + } + + @Override + public ECPublicKey getPublicKeyById(String kid) { + return (ECPublicKey) getKeyInstance(key, "EC", false); + } + + @Override + public ECPrivateKey getPrivateKey() { + return (ECPrivateKey) getKeyInstance(key, "EC", true); + } + + @Override + public String getPrivateKeyId() { + return "id"; + } + } + + private void throwUnsupportedAlgo() { + throw new AlgorithmMismatchException("Unsupported algorithm '" + algorithmName + "'"); + } + +} diff --git a/src/main/java/app/controllers/HighLightController.java b/src/main/java/app/controllers/HighLightController.java new file mode 100644 index 0000000..f169a90 --- /dev/null +++ b/src/main/java/app/controllers/HighLightController.java @@ -0,0 +1,41 @@ +package app.controllers; + +import burp.api.montoya.core.Annotations; +import burp.api.montoya.core.HighlightColor; +import burp.api.montoya.http.handler.*; + +import app.helpers.Config; +import app.tokenposition.ITokenPosition; + +// This controller handles the highlighting of entries in the HTTP history tab +public class HighLightController implements HttpHandler { + + @Override + public RequestToBeSentAction handleHttpRequestToBeSent(HttpRequestToBeSent requestToBeSent) { + boolean containsJWT = ITokenPosition.findTokenPositionImplementation(requestToBeSent, true) != null; + if (containsJWT) { + updateAnnotations(requestToBeSent.annotations()); + } + + return RequestToBeSentAction.continueWith(requestToBeSent); + } + + @Override + public ResponseReceivedAction handleHttpResponseReceived(HttpResponseReceived responseReceived) { + boolean containsJWT = ITokenPosition.findTokenPositionImplementation(responseReceived, false) != null; + if (containsJWT) { + updateAnnotations(responseReceived.annotations()); + } + + return ResponseReceivedAction.continueWith(responseReceived); + } + + private void updateAnnotations(Annotations annotations) { + if (!Config.interceptComment.isEmpty()) { + annotations.setNotes(Config.interceptComment); + } + if (!Config.highlightColor.equals("None")) { + annotations.setHighlightColor(HighlightColor.highlightColor(Config.highlightColor)); + } + } +} diff --git a/src/main/java/app/controllers/JWTInterceptTabController.java b/src/main/java/app/controllers/JWTInterceptTabController.java new file mode 100644 index 0000000..aca0def --- /dev/null +++ b/src/main/java/app/controllers/JWTInterceptTabController.java @@ -0,0 +1,536 @@ +package app.controllers; + +import java.awt.Component; +import java.awt.FileDialog; +import java.awt.Frame; +import java.awt.event.ActionListener; +import java.awt.event.KeyEvent; +import java.awt.event.KeyListener; +import java.nio.charset.StandardCharsets; +import java.security.interfaces.RSAPublicKey; +import java.util.Base64; +import java.util.List; +import java.util.Objects; + +import javax.swing.JCheckBox; +import javax.swing.JComboBox; +import javax.swing.SwingUtilities; +import javax.swing.event.DocumentEvent; +import javax.swing.event.DocumentListener; + +import app.helpers.*; +import burp.api.montoya.core.ByteArray; +import burp.api.montoya.http.message.HttpMessage; +import burp.api.montoya.http.message.HttpRequestResponse; +import burp.api.montoya.http.message.requests.HttpRequest; +import burp.api.montoya.http.message.responses.HttpResponse; +import burp.api.montoya.ui.Selection; +import burp.api.montoya.ui.editor.extension.ExtensionProvidedHttpRequestEditor; +import burp.api.montoya.ui.editor.extension.ExtensionProvidedHttpResponseEditor; +import org.apache.commons.codec.binary.Hex; + +import com.auth0.jwt.algorithms.Algorithm; +import com.eclipsesource.json.Json; +import com.eclipsesource.json.JsonObject; + +import app.algorithm.AlgorithmWrapper; +import app.tokenposition.ITokenPosition; +import gui.JWTInterceptTab; +import model.CustomJWToken; +import model.JWTInterceptModel; +import model.Settings; +import model.Strings; +import model.TimeClaim; + +// used in the proxy intercept and repeater tabs +public class JWTInterceptTabController implements ExtensionProvidedHttpRequestEditor, ExtensionProvidedHttpResponseEditor { + + private final JWTInterceptModel jwtIM; + private final JWTInterceptTab jwtST; + private ITokenPosition tokenPosition; + private boolean resignOnType; + private boolean randomKey; + private boolean chooseSignature; + private boolean recalculateSignature; + private String algAttackMode; + private boolean cveAttackMode; + private boolean edited; + private String originalSignature; + private boolean addMetaHeader; + private static final String HEX_MARKER = "0x"; + private final boolean isRequest; + private HttpRequestResponse httpRequestResponse; + + public JWTInterceptTabController(JWTInterceptModel jwIM, JWTInterceptTab jwtST, boolean isRequest) { + this.jwtIM = jwIM; + this.jwtST = jwtST; + this.isRequest = isRequest; + createAndRegisterActionListeners(jwtST); + } + + private void cveAttackChanged() { + edited = true; + JCheckBox jcb = jwtST.getCVEAttackCheckBox(); + cveAttackMode = jcb.isSelected(); + jwtST.getNoneAttackComboBox().setEnabled(!cveAttackMode); + jwtST.getRdbtnDontModify().setEnabled(!cveAttackMode); + jwtST.getRdbtnOriginalSignature().setEnabled(!cveAttackMode); + jwtST.getRdbtnRandomKey().setEnabled(!cveAttackMode); + jwtST.getRdbtnRecalculateSignature().setEnabled(!cveAttackMode); + jwtST.setKeyFieldState(!cveAttackMode); + jwtST.getCVECopyBtn().setVisible(cveAttackMode); + + if (cveAttackMode) { + jwtST.getRdbtnDontModify().setSelected(true); + jwtST.getRdbtnOriginalSignature().setSelected(false); + jwtST.getRdbtnRandomKey().setSelected(false); + jwtST.getRdbtnRecalculateSignature().setSelected(false); + CustomJWToken token = jwtIM.getJwToken(); + String headerJSON = token.getHeaderJson(); + JsonObject headerJSONObj = Json.parse(headerJSON).asObject(); + headerJSONObj.set("alg", "RS256"); + JsonObject jwk = new JsonObject(); + jwk.add("kty", "RSA"); + jwk.add("kid", "jwt4b@portswigger.net"); + jwk.add("use", "sig"); + RSAPublicKey pk = KeyHelper.loadCVEAttackPublicKey(); + jwk.add("n", Base64.getUrlEncoder().encodeToString(pk.getPublicExponent().toByteArray())); + jwk.add("e", Base64.getUrlEncoder().encodeToString(pk.getModulus().toByteArray())); + headerJSONObj.add("jwk", jwk); + token.setHeaderJson(headerJSONObj.toString()); + try { + Algorithm algo = AlgorithmWrapper.getSignerAlgorithm(token.getAlgorithm(), Config.cveAttackModePrivateKey); + token.calculateAndSetSignature(algo); + reflectChangeToView(token, true); + } catch (Exception e) { + reportError("Failed to sign when using cve attack mode - " + e.getMessage()); + } + } else { + jwtST.setKeyFieldValue(""); + jwtST.setKeyFieldState(false); + CustomJWToken customJWToken = new CustomJWToken(jwtIM.getOriginalJWT()); + jwtIM.setJwToken(customJWToken); + jwtST.getNoneAttackComboBox().setSelectedIndex(0); + reflectChangeToView(jwtIM.getJwToken(), true); + } + } + + private void signKeyChange() { + jwtIM.setJWTSignatureKey(jwtST.getKeyFieldValue()); + try { + if (jwtIM.getJWTKey() != null && jwtIM.getJWTKey().length() > 0 && jwtST.getKeyField().isEnabled()) { + String key = getKeyWithHexDetection(jwtIM); + Output.output("Signing with manually entered key '" + key + "' (" + jwtIM.getJWTKey() + ")"); + CustomJWToken token = ReadableTokenFormat.getTokenFromView(jwtST); + + if (Config.o365Support && O365.isO365Request(token, token.getAlgorithm())) { + O365.handleO365(key, token); + reflectChangeToView(token, false); + } else { + Algorithm algo = AlgorithmWrapper.getSignerAlgorithm(token.getAlgorithm(), key); + token.calculateAndSetSignature(algo); + reflectChangeToView(token, false); + } + clearError(); + } + } catch (Exception e) { + int len = 8; + String key = jwtST.getKeyFieldValue(); + key = key.length() > len ? key.substring(0, len) + "..." : key; + reportError("Cannot sign with key " + key + " - " + e.getMessage()); + } + } + + private String getKeyWithHexDetection(JWTInterceptModel jwtIM) { + String key = jwtIM.getJWTKey(); + if (key.startsWith(HEX_MARKER)) { + try { + key = key.substring(2); + byte[] bytes = Hex.decodeHex(key); + key = new String(bytes, StandardCharsets.ISO_8859_1); + } catch (Exception e) { + key = jwtIM.getJWTKey(); + } + } + return key; + } + + private void algAttackChanged() { + edited = true; + JComboBox jCB = jwtST.getNoneAttackComboBox(); + switch (jCB.getSelectedIndex()) { + default: + case 0: + algAttackMode = null; + break; + case 1: + algAttackMode = "none"; + break; + case 2: + algAttackMode = "None"; + break; + case 3: + algAttackMode = "nOnE"; + break; + case 4: + algAttackMode = "NONE"; + break; + } + + CustomJWToken token = jwtIM.getJwToken(); + String header = token.getHeaderJson(); + if (algAttackMode == null) { + Output.output("Resetting alg attack mode - " + jwtIM.getOriginalJWToken().getAlgorithm()); + token.setHeaderJson(header.replace(token.getAlgorithm(), jwtIM.getOriginalJWToken().getAlgorithm())); + token.setSignature(originalSignature); + jwtST.getRdbtnDontModify().setSelected(true); + radioButtonChanged(true, false, false, false, false); + jwtST.setRadiosState(true); + jwtST.setKeyFieldState(true); + } else { + jwtST.getRdbtnDontModify().setSelected(true); + jwtST.setRadiosState(false); + jwtST.setKeyFieldState(false); + token.setSignature(""); + token.setHeaderJson(header.replace(token.getAlgorithm(), algAttackMode)); + } + reflectChangeToView(token, true); + } + + private void radioButtonChanged(boolean cDM, boolean cRK, boolean cOS, boolean cRS, boolean cCS) { + clearError(); + resignOnType = !cDM && !cOS; + boolean oldRandomKey = randomKey; + edited = true; + addMetaHeader = false; + boolean dontModifySignature = jwtST.getRdbtnDontModify().isSelected(); + randomKey = jwtST.getRdbtnRandomKey().isSelected(); + boolean keepOriginalSignature = jwtST.getRdbtnOriginalSignature().isSelected(); + recalculateSignature = jwtST.getRdbtnRecalculateSignature().isSelected(); + chooseSignature = jwtST.getRdbtnChooseSignature().isSelected(); + + jwtST.setKeyFieldState(!keepOriginalSignature && !dontModifySignature && !randomKey && !chooseSignature); + if (keepOriginalSignature) { + CustomJWToken origSignatureToken = jwtIM.getJwToken().setSignature(originalSignature); + jwtIM.setJwToken(origSignatureToken); + jwtIM.setJWTSignatureKey(""); + jwtST.setKeyFieldValue(""); + jwtST.updateSetView(false); + } else if (dontModifySignature) { + jwtIM.setJWTSignatureKey(""); + jwtST.setKeyFieldValue(""); + } else if (randomKey) { + addMetaHeader = true; + if (!oldRandomKey) { + generateRandomKey(); + } + } else if (cCS) { + FileDialog dialog = new FileDialog((Frame) null, "Select File to Open"); + dialog.setMode(FileDialog.LOAD); + dialog.setVisible(true); + if (dialog.getFile() != null) { + String file = dialog.getDirectory() + dialog.getFile(); + Output.output(file + " chosen."); + String chosen = Strings.filePathToString(file); + // TODO error will be redirected to Output.outputError, but make sure this case + // will be shown in UI too + jwtIM.setJWTSignatureKey(chosen); + jwtST.setKeyFieldValue(chosen); + jwtST.updateSetView(false); + } + } else if ((recalculateSignature || chooseSignature)) { + if (recalculateSignature) { + String cleanKey = KeyHelper.cleanKey(jwtST.getKeyFieldValue()); + jwtIM.setJWTSignatureKey(cleanKey); + } + Algorithm algo; + try { + if (jwtIM.getJWTKey().length() > 0) { + CustomJWToken token = jwtIM.getJwToken(); + Output.output("Recalculating Signature with Secret - '" + jwtIM.getJWTKey() + "'"); + algo = AlgorithmWrapper.getSignerAlgorithm(token.getAlgorithm(), jwtIM.getJWTKey()); + token.calculateAndSetSignature(algo); + reflectChangeToView(token, true); + addMetaHeader = true; + } + } catch (IllegalArgumentException e) { + reportError("Exception while recalculating signature - " + e.getMessage()); + } + } + } + + private void clearError() { + jwtIM.setProblemDetail(""); + jwtST.setProblemLbl(""); + } + + private void reportError(String error) { + Output.outputError(error); + jwtIM.setProblemDetail(error); + jwtST.setProblemLbl(jwtIM.getProblemDetail()); + } + + private void generateRandomKey() { + SwingUtilities.invokeLater(() -> { + try { + CustomJWToken token = ReadableTokenFormat.getTokenFromView(jwtST); + String generatedRandomKey = KeyHelper.getRandomKey(token.getAlgorithm()); + Output.output("Generating Random Key for Signature Calculation: " + generatedRandomKey); + jwtIM.setJWTSignatureKey(generatedRandomKey); + Algorithm algo = AlgorithmWrapper.getSignerAlgorithm(token.getAlgorithm(), generatedRandomKey); + token.calculateAndSetSignature(algo); + jwtIM.setJwToken(token); + jwtST.setKeyFieldValue(generatedRandomKey); + jwtST.updateSetView(false); + } catch (Exception e) { + reportError("Exception during random key generation & signing: " + e.getMessage()); + } + }); + } + + private void replaceTokenInMessage() { + CustomJWToken token; + if (!jwtInUIisValid()) { + Output.outputError("Wont replace JWT as invalid"); + edited = false; + return; + } + try { + token = ReadableTokenFormat.getTokenFromView(jwtST); + Output.output("Replacing token: " + token.getToken()); + // token may be null, if it is invalid JSON, if so, don't try changing anything + if (token.getToken() != null) { + this.tokenPosition.replaceToken(token.getToken()); + } + } catch (Exception e) { + // TODO is this visible to user? + reportError("Could not replace token in message: " + e.getMessage()); + } + } + + private void addLogHeadersToRequest() { + if (addMetaHeader) { + this.tokenPosition.cleanJWTHeaders(); + this.tokenPosition.addHeader(Strings.JWT_HEADER_PREFIX, Strings.JWT_HEADER_INFO); + this.tokenPosition.addHeader(Strings.JWT_HEADER_PREFIX, "SIGNER-KEY " + jwtIM.getJWTKey()); + if (PublicKeyBroker.publicKey != null) { + this.tokenPosition.addHeader(Strings.JWT_HEADER_PREFIX, "SIGNER-PUBLIC-KEY " + PublicKeyBroker.publicKey); + PublicKeyBroker.publicKey = null; + } + } + } + + private void reflectChangeToView(CustomJWToken token, boolean updateKey) { + jwtIM.setJwToken(token); + jwtST.updateSetView(false, updateKey); + } + + private void handleJWTAreaTyped() { + if (jwtInUIisValid()) { + clearError(); + } else { + // TODO determine error more accurately, what exactly is wrong with the jwt + reportError("invalid JWT"); + return; + } + if (resignOnType) { + Output.output("Recalculating signature as key typed"); + CustomJWToken token = null; + try { + token = ReadableTokenFormat.getTokenFromView(jwtST); + } catch (Exception e) { + reportError("JWT can't be parsed - " + e.getMessage()); + } + if (jwtIM.getJWTKey().length() == 0) { + reportError("Can't resign with an empty key"); + } + try { + Algorithm algo = AlgorithmWrapper.getSignerAlgorithm(Objects.requireNonNull(token).getAlgorithm(), jwtIM.getJWTKey()); + token.calculateAndSetSignature(algo); + jwtIM.setJwToken(token); + jwtST.getJwtSignatureArea().setText(jwtIM.getJwToken().getSignature()); + } catch (Exception e) { + reportError("Could not resign: " + e.getMessage()); + } + } + } + + public boolean jwtInUIisValid() { + boolean valid = false; + try { + CustomJWToken tokenFromView = ReadableTokenFormat.getTokenFromView(jwtST); + if (tokenFromView.getHeaderJsonNode().get("alg") != null) { + valid = CustomJWToken.isValidJWT(tokenFromView.getToken()); + } + } catch (Exception ignored) { + // ignored + } + return valid; + } + + protected boolean hasNotChanged() { + // see + // https://github.com/PortSwigger/example-custom-editor-tab/blob/master/java/BurpExtender.java#L119 + return !edited && !recalculateSignature && !randomKey && !chooseSignature && algAttackMode == null && !cveAttackMode; + } + + protected void performUpdate() { + clearError(); + radioButtonChanged(true, false, false, false, false); + jwtST.getCVEAttackCheckBox().setSelected(false); + replaceTokenInMessage(); + addLogHeadersToRequest(); + } + + @Override + public HttpRequest getRequest() { + if (hasNotChanged()) { + return httpRequestResponse.request(); + } + + performUpdate(); + return this.tokenPosition.getRequest(); + } + + @Override + public HttpResponse getResponse() { + if (hasNotChanged()) { + return httpRequestResponse.response(); + } + + performUpdate(); + return this.tokenPosition.getResponse(); + } + + @Override + public void setRequestResponse(HttpRequestResponse requestResponse) { + httpRequestResponse = requestResponse; + HttpMessage httpMessage; + + if (isRequest) { + httpMessage = httpRequestResponse.request(); + } else { + httpMessage = httpRequestResponse.response(); + } + + edited = false; + tokenPosition = ITokenPosition.findTokenPositionImplementation(httpMessage, isRequest); + boolean messageContainsJWT = tokenPosition != null; + if (messageContainsJWT) { + String token = tokenPosition.getToken(); + CustomJWToken cJWT = new CustomJWToken(token); + jwtIM.setOriginalJWToken(new CustomJWToken(token)); + jwtIM.setOriginalJWT(token); + List tcl = cJWT.getTimeClaimList(); + jwtIM.setTimeClaims(tcl); + jwtIM.setJwToken(cJWT); + originalSignature = cJWT.getSignature(); + jwtIM.setcFW(tokenPosition.getcFW()); + jwtST.updateSetView(Config.resetEditor); + algAttackMode = null; + if (Config.resetEditor) { + jwtST.getNoneAttackComboBox().setSelectedIndex(0); + } + } else { + jwtST.updateSetView(true); + } + } + + @Override + public boolean isEnabledFor(HttpRequestResponse requestResponse) { + HttpMessage httpMessage; + + if (this.isRequest) { + httpMessage = requestResponse.request(); + } else { + httpMessage = requestResponse.response(); + } + + return ITokenPosition.findTokenPositionImplementation(httpMessage, this.isRequest) != null; + } + + @Override + public String caption() { + return Settings.TAB_NAME; + } + + @Override + public Component uiComponent() { + return jwtST; + } + + @Override + public Selection selectedData() { + return Selection.selection(ByteArray.byteArray(jwtST.getSelectedData().getBytes())); + } + + @Override + public boolean isModified() { + return edited; + } + + private void createAndRegisterActionListeners(JWTInterceptTab jwtST) { + + KeyListener editedKeyListener = new KeyListener() { + + @Override + public void keyTyped(KeyEvent arg0) { + } + + @Override + public void keyReleased(KeyEvent arg0) { + } + + @Override + public void keyPressed(KeyEvent arg0) { + edited = true; + } + }; + + jwtST.getJwtPayloadArea().addKeyListener(editedKeyListener); + + ActionListener dontModifyListener = e -> radioButtonChanged(true, false, false, false, false); + ActionListener randomKeyListener = e -> radioButtonChanged(false, true, false, false, false); + ActionListener originalSignatureListener = e -> radioButtonChanged(false, false, true, false, false); + ActionListener recalculateSignatureListener = e -> radioButtonChanged(false, false, false, true, false); + ActionListener chooseSignatureListener = e -> radioButtonChanged(false, false, false, false, true); + ActionListener algAttackListener = e -> algAttackChanged(); + ActionListener cveAttackListener = e -> cveAttackChanged(); + + DocumentListener jwtKeyChanged = new DelayedDocumentListener(new DocumentListener() { + + @Override + public void insertUpdate(DocumentEvent e) { + signKeyChange(); + } + + @Override + public void removeUpdate(DocumentEvent e) { + signKeyChange(); + } + + @Override + public void changedUpdate(DocumentEvent e) { + } + }); + + KeyListener jwtAreaTyped = new KeyListener() { + + @Override + public void keyTyped(KeyEvent e) { + } + + @Override + public void keyPressed(KeyEvent e) { + } + + @Override + public void keyReleased(KeyEvent e) { + handleJWTAreaTyped(); + } + }; + + jwtST.registerActionListeners(dontModifyListener, randomKeyListener, originalSignatureListener, recalculateSignatureListener, chooseSignatureListener, algAttackListener, cveAttackListener, + jwtKeyChanged, jwtAreaTyped); + } +} diff --git a/src/main/java/app/controllers/JWTRequestInterceptTabController.java b/src/main/java/app/controllers/JWTRequestInterceptTabController.java new file mode 100644 index 0000000..4372ed1 --- /dev/null +++ b/src/main/java/app/controllers/JWTRequestInterceptTabController.java @@ -0,0 +1,12 @@ +package app.controllers; + +import gui.JWTInterceptTab; +import model.JWTInterceptModel; + +public class JWTRequestInterceptTabController extends JWTInterceptTabController { + + public JWTRequestInterceptTabController(JWTInterceptModel jwtTM, JWTInterceptTab jwtVT) { + super(jwtTM, jwtVT, true); + } + +} \ No newline at end of file diff --git a/src/main/java/app/controllers/JWTRequestTabController.java b/src/main/java/app/controllers/JWTRequestTabController.java new file mode 100644 index 0000000..6130b6d --- /dev/null +++ b/src/main/java/app/controllers/JWTRequestTabController.java @@ -0,0 +1,12 @@ +package app.controllers; + +import gui.JWTViewTab; +import model.JWTTabModel; + +public class JWTRequestTabController extends JwtTabController { + + public JWTRequestTabController(JWTTabModel jwtTM, JWTViewTab jwtVT) { + super(jwtTM, jwtVT, true); + } + +} diff --git a/src/main/java/app/controllers/JWTResponseInterceptTabController.java b/src/main/java/app/controllers/JWTResponseInterceptTabController.java new file mode 100644 index 0000000..80ed1b1 --- /dev/null +++ b/src/main/java/app/controllers/JWTResponseInterceptTabController.java @@ -0,0 +1,12 @@ +package app.controllers; + +import gui.JWTInterceptTab; +import model.JWTInterceptModel; + +public class JWTResponseInterceptTabController extends JWTInterceptTabController { + + public JWTResponseInterceptTabController(JWTInterceptModel jwtTM, JWTInterceptTab jwtVT) { + super(jwtTM, jwtVT, false); + } + +} diff --git a/src/main/java/app/controllers/JWTResponseTabController.java b/src/main/java/app/controllers/JWTResponseTabController.java new file mode 100644 index 0000000..8c8fd21 --- /dev/null +++ b/src/main/java/app/controllers/JWTResponseTabController.java @@ -0,0 +1,12 @@ +package app.controllers; + +import gui.JWTViewTab; +import model.JWTTabModel; + +public class JWTResponseTabController extends JwtTabController { + + public JWTResponseTabController(JWTTabModel jwtTM, JWTViewTab jwtVT) { + super(jwtTM, jwtVT, false); + } + +} diff --git a/src/main/java/app/controllers/JWTSuiteTabController.java b/src/main/java/app/controllers/JWTSuiteTabController.java new file mode 100644 index 0000000..91107b8 --- /dev/null +++ b/src/main/java/app/controllers/JWTSuiteTabController.java @@ -0,0 +1,163 @@ +package app.controllers; + +import java.awt.Component; +import java.util.List; + +import javax.swing.JTabbedPane; +import javax.swing.event.DocumentEvent; +import javax.swing.event.DocumentListener; + +import app.algorithm.AlgorithmWrapper; +import com.auth0.jwt.JWT; +import com.auth0.jwt.JWTVerifier; +import com.auth0.jwt.exceptions.InvalidClaimException; +import com.auth0.jwt.exceptions.JWTVerificationException; +import com.auth0.jwt.exceptions.SignatureVerificationException; +import com.auth0.jwt.interfaces.DecodedJWT; + +import app.helpers.Output; +import gui.JWTSuiteTab; +import model.CustomJWToken; +import model.JWTSuiteTabModel; +import model.Settings; +import model.Strings; +import model.TimeClaim; + +// used to provide the standalone suite tab after "User Options" +public class JWTSuiteTabController { + + private final JWTSuiteTabModel jwtSTM; + private final JWTSuiteTab jwtST; + + public JWTSuiteTabController(final JWTSuiteTabModel jwtSTM, final JWTSuiteTab jwtST) { + this.jwtSTM = jwtSTM; + this.jwtST = jwtST; + + createAndRegisterActionListeners(jwtSTM, jwtST); + } + + // This method was copied from + // https://support.portswigger.net/customer/portal/questions/16743551-burp-extension-get-focus-on-tab-after-custom-menu-action + public void selectJWTSuiteTab() { + Component current = jwtST; + do { + current = current.getParent(); + } while (!(current instanceof JTabbedPane)); + + JTabbedPane tabPane = (JTabbedPane) current; + for (int i = 0; i < tabPane.getTabCount(); i++) { + if (tabPane.getTitleAt(i).equals(Settings.TAB_NAME)) + tabPane.setSelectedIndex(i); + } + } + + public void contextActionSendJWTtoSuiteTab(String jwts, boolean fromContextMenu) { + jwts = jwts.replace("Authorization:", ""); + jwts = jwts.replace("Bearer", ""); + jwts = jwts.replace("Set-Cookie: ", ""); + jwts = jwts.replace("Cookie: ", ""); + jwts = jwts.replaceAll("\\s", ""); + jwtSTM.setJwtInput(jwts); + try { + CustomJWToken jwt = new CustomJWToken(jwts); + List tcl = jwt.getTimeClaimList(); + jwtSTM.setTimeClaims(tcl); + jwtSTM.setJwtJSON(ReadableTokenFormat.getReadableFormat(jwt)); + } catch (Exception e) { + Output.outputError("JWT Context Action: " + e.getMessage()); + } + if (fromContextMenu) { + // Reset View and Select + jwtSTM.setJwtKey(""); + selectJWTSuiteTab(); + } else { + // Since we changed the JWT, we need to check the Key/Signature too + contextActionKey(jwtSTM.getJwtKey()); + } + jwtST.updateSetView(); + } + + public void contextActionKey(String key) { + jwtSTM.setJwtKey(key); + jwtSTM.setVerificationResult(""); + try { + CustomJWToken token = new CustomJWToken(jwtSTM.getJwtInput()); + String curAlgo = token.getAlgorithm(); + JWTVerifier verifier = JWT.require(AlgorithmWrapper.getVerifierAlgorithm(curAlgo, key)).build(); + DecodedJWT test = verifier.verify(token.getToken()); + jwtSTM.setJwtSignatureColor(Settings.getValidColor()); + jwtSTM.setVerificationLabel(Strings.VALID_VERFICIATION); + test.getAlgorithm(); + } catch (JWTVerificationException e) { + Output.output("Verification failed (" + e.getMessage() + ")"); + jwtSTM.setVerificationResult(e.getMessage()); + + if (e instanceof SignatureVerificationException) { + jwtSTM.setJwtSignatureColor(Settings.getInvalidColor()); + jwtSTM.setVerificationLabel(Strings.INVALID_SIGNATURE_VERIFICATION); + } else if (e instanceof InvalidClaimException) { + jwtSTM.setJwtSignatureColor(Settings.getProblemColor()); + jwtSTM.setVerificationLabel(Strings.INVALID_CLAIM_VERIFICATION); + } else { + jwtSTM.setJwtSignatureColor(Settings.getProblemColor()); + jwtSTM.setVerificationLabel(Strings.GENERIC_ERROR_VERIFICATION); + } + + } catch (IllegalArgumentException e) { + Output.output("Verification failed (" + e.getMessage() + ")"); + jwtSTM.setJwtSignatureColor(Settings.getProblemColor()); + jwtSTM.setVerificationResult(e.getMessage()); + jwtSTM.setVerificationLabel(Strings.INVALID_KEY_VERIFICATION); + } + jwtST.updateSetView(); + } + + private void createAndRegisterActionListeners(final JWTSuiteTabModel jwtSTM, final JWTSuiteTab jwtST) { + DocumentListener jwtDocInputListener = new DocumentListener() { + + @Override + public void removeUpdate(DocumentEvent e) { + propagateUpdate(jwtSTM, jwtST); + } + + @Override + public void insertUpdate(DocumentEvent e) { + propagateUpdate(jwtSTM, jwtST); + } + + private void propagateUpdate(final JWTSuiteTabModel jwtSTM, final JWTSuiteTab jwtST) { + jwtSTM.setJwtInput(jwtST.getJWTInput()); + contextActionSendJWTtoSuiteTab(jwtSTM.getJwtInput(), false); + } + + @Override + public void changedUpdate(DocumentEvent ignored) { + // not required + } + }; + DocumentListener jwtDocKeyListener = new DocumentListener() { + + @Override + public void removeUpdate(DocumentEvent e) { + propagateUpdate(jwtSTM, jwtST); + } + + @Override + public void insertUpdate(DocumentEvent e) { + propagateUpdate(jwtSTM, jwtST); + } + + private void propagateUpdate(final JWTSuiteTabModel jwtSTM, final JWTSuiteTab jwtST) { + jwtSTM.setJwtKey(jwtST.getKeyInput()); + contextActionKey(jwtSTM.getJwtKey()); + } + + @Override + public void changedUpdate(DocumentEvent ignored) { + // not required + } + }; + + jwtST.registerDocumentListener(jwtDocInputListener, jwtDocKeyListener); + } +} diff --git a/src/main/java/app/controllers/JwtTabController.java b/src/main/java/app/controllers/JwtTabController.java new file mode 100644 index 0000000..1aaed85 --- /dev/null +++ b/src/main/java/app/controllers/JwtTabController.java @@ -0,0 +1,212 @@ +package app.controllers; + +import java.awt.Component; +import java.util.ArrayList; +import java.util.List; +import java.util.Objects; + +import javax.swing.event.DocumentEvent; +import javax.swing.event.DocumentListener; + +import burp.api.montoya.core.ByteArray; +import burp.api.montoya.http.message.HttpMessage; +import burp.api.montoya.http.message.HttpRequestResponse; +import burp.api.montoya.http.message.requests.HttpRequest; +import burp.api.montoya.http.message.responses.HttpResponse; +import burp.api.montoya.ui.Selection; +import burp.api.montoya.ui.editor.extension.ExtensionProvidedHttpRequestEditor; +import burp.api.montoya.ui.editor.extension.ExtensionProvidedHttpResponseEditor; +import com.auth0.jwt.JWT; +import com.auth0.jwt.JWTVerifier; +import com.auth0.jwt.exceptions.InvalidClaimException; +import com.auth0.jwt.exceptions.JWTVerificationException; +import com.auth0.jwt.exceptions.SignatureVerificationException; +import com.auth0.jwt.interfaces.DecodedJWT; + +import app.algorithm.AlgorithmType; +import app.algorithm.AlgorithmWrapper; +import app.helpers.Output; +import app.tokenposition.ITokenPosition; +import gui.JWTViewTab; +import model.CustomJWToken; +import model.JWTTabModel; +import model.Settings; +import model.Strings; +import model.TimeClaim; + +// view to check JWTs such as in the HTTP history +public class JwtTabController implements ExtensionProvidedHttpRequestEditor, ExtensionProvidedHttpResponseEditor { + + private ITokenPosition tokenPosition; + private final ArrayList modelStateList = new ArrayList(); + private byte[] content; + private final JWTTabModel jwtTM; + private final JWTViewTab jwtVT; + boolean isRequest; + private HttpRequestResponse httpRequestResponse; + + public JwtTabController(final JWTTabModel jwtTM, final JWTViewTab jwtVT, boolean isRequest) { + this.jwtTM = jwtTM; + this.jwtVT = jwtVT; + this.isRequest = isRequest; + + DocumentListener documentListener = new DocumentListener() { + + @Override + public void removeUpdate(DocumentEvent arg0) { + jwtTM.setKey(jwtVT.getKeyValue()); + checkKey(jwtTM.getKey()); + } + + @Override + public void insertUpdate(DocumentEvent arg0) { + jwtTM.setKey(jwtVT.getKeyValue()); + checkKey(jwtTM.getKey()); + } + + @Override + public void changedUpdate(DocumentEvent arg0) { + jwtTM.setKey(jwtVT.getKeyValue()); + checkKey(jwtTM.getKey()); + } + }; + + jwtVT.registerDocumentListener(documentListener); + } + + @Override + public HttpRequest getRequest() { + return httpRequestResponse.request(); + } + + @Override + public HttpResponse getResponse() { + return httpRequestResponse.response(); + } + + @Override + public void setRequestResponse(HttpRequestResponse requestResponse) { + httpRequestResponse = requestResponse; + HttpMessage httpMessage; + + if (isRequest) { + httpMessage = requestResponse.request(); + } else { + httpMessage = requestResponse.response(); + } + + try { + tokenPosition = ITokenPosition.findTokenPositionImplementation(httpMessage, this.isRequest); + jwtTM.setJWT(Objects.requireNonNull(tokenPosition).getToken()); + } catch (Exception e) { + Output.outputError("Exception setting message: " + e.getMessage()); + } + CustomJWToken jwt = new CustomJWToken(jwtTM.getJWT()); + jwtTM.setJWTJSON(ReadableTokenFormat.getReadableFormat(jwt)); + List tcl = jwt.getTimeClaimList(); + jwtTM.setTimeClaims(tcl); + if (tokenPosition != null) { + jwtTM.setcFW(tokenPosition.getcFW()); + } + + JWTTabModel current = new JWTTabModel(jwtTM.getKey(), content); + int containsIndex = modelStateList.indexOf(current); + + // we know this request, load it + if (containsIndex != -1) { + JWTTabModel knownModel = modelStateList.get(containsIndex); + jwtTM.setKey(knownModel.getKey()); + jwtTM.setVerificationColor(knownModel.getVerificationColor()); + jwtTM.setVerificationLabel(knownModel.getVerificationLabel()); + // we haven't seen this request yet, add it and set the view to + // default + } else { + modelStateList.add(current); + jwtTM.setVerificationColor(Settings.COLOR_UNDEFINED); + jwtTM.setVerificationResult(""); + jwtTM.setKey(""); + } + AlgorithmType algoType = AlgorithmWrapper.getTypeOf(getCurrentAlgorithm()); + jwtVT.updateSetView(algoType); + } + + @Override + public boolean isEnabledFor(HttpRequestResponse requestResponse) { + HttpMessage message; + + if (this.isRequest) { + this.content = requestResponse.request().toString().getBytes(); + message = requestResponse.request(); + } else { + this.content = requestResponse.response().toString().getBytes(); + message = requestResponse.response(); + } + + return ITokenPosition.findTokenPositionImplementation(message, this.isRequest) != null; + } + + @Override + public String caption() { + return Settings.TAB_NAME; + } + + @Override + public Component uiComponent() { + return this.jwtVT; + } + + @Override + public Selection selectedData() { + return Selection.selection(ByteArray.byteArray(jwtVT.getSelectedData().getBytes())); + } + + @Override + public boolean isModified() { + return false; + } + + public void checkKey(String key) { + jwtTM.setVerificationResult(""); + String curAlgo = getCurrentAlgorithm(); + AlgorithmType algoType = AlgorithmWrapper.getTypeOf(getCurrentAlgorithm()); + try { + JWTVerifier verifier = JWT.require(AlgorithmWrapper.getVerifierAlgorithm(curAlgo, key)).build(); + DecodedJWT test = verifier.verify(jwtTM.getJWT()); + jwtTM.setVerificationLabel(Strings.VALID_VERFICIATION); + jwtTM.setVerificationColor(Settings.getValidColor()); + test.getAlgorithm(); + jwtVT.updateSetView(algoType); + } catch (JWTVerificationException e) { + if (e instanceof SignatureVerificationException) { + jwtTM.setVerificationColor(Settings.getInvalidColor()); + jwtTM.setVerificationLabel(Strings.INVALID_SIGNATURE_VERIFICATION); + } else if (e instanceof InvalidClaimException) { + jwtTM.setVerificationColor(Settings.getProblemColor()); + jwtTM.setVerificationLabel(Strings.INVALID_CLAIM_VERIFICATION); + } else { + jwtTM.setVerificationColor(Settings.getProblemColor()); + jwtTM.setVerificationLabel(Strings.GENERIC_ERROR_VERIFICATION); + } + jwtTM.setVerificationResult(e.getMessage()); + jwtVT.updateSetView(algoType); + } catch (IllegalArgumentException e) { + jwtTM.setVerificationResult(e.getMessage()); + jwtTM.setVerificationLabel(Strings.INVALID_KEY_VERIFICATION); + jwtTM.setVerificationColor(Settings.getProblemColor()); + jwtVT.updateSetView(algoType); + } + JWTTabModel current = new JWTTabModel(key, content); + int containsIndex = modelStateList.indexOf(current); + if (containsIndex != -1) { // we know this request, update the viewstate + JWTTabModel knownState = modelStateList.get(containsIndex); + knownState.setKeyValueAndHash(key, current.getHashCode()); + knownState.setVerificationResult(jwtTM.getVerificationLabel()); + knownState.setVerificationColor(jwtTM.getVerificationColor()); + } + } + + public String getCurrentAlgorithm() { + return new CustomJWToken(jwtTM.getJWT()).getAlgorithm(); + } + +} diff --git a/src/main/java/app/controllers/ReadableTokenFormat.java b/src/main/java/app/controllers/ReadableTokenFormat.java new file mode 100644 index 0000000..aa866ed --- /dev/null +++ b/src/main/java/app/controllers/ReadableTokenFormat.java @@ -0,0 +1,59 @@ +package app.controllers; + +import static com.eclipsesource.json.WriterConfig.PRETTY_PRINT; + +import static org.apache.commons.lang.StringUtils.isBlank; + +import com.eclipsesource.json.Json; +import com.eclipsesource.json.JsonValue; + +import app.helpers.Output; +import gui.JWTInterceptTab; +import model.CustomJWToken; + +public class ReadableTokenFormat { + + ReadableTokenFormat() { + + } + + private static final String NEW_LINE = System.getProperty("line.separator"); + private static final String TITTLE_HEADERS = "Headers = "; + private static final String TITLE_PAYLOAD = NEW_LINE + NEW_LINE + "Payload = "; + private static final String TITLE_SIGNATURE = NEW_LINE + NEW_LINE + "Signature = "; + + public static String getReadableFormat(CustomJWToken token) { + + return TITTLE_HEADERS + jsonBeautify(token.getHeaderJson()) + TITLE_PAYLOAD + jsonBeautify(token.getPayloadJson()) + TITLE_SIGNATURE + "\"" + token.getSignature() + "\""; + } + + public static String jsonBeautify(String input) { + if (isBlank(input)) { + return ""; + } + + try { + JsonValue value = Json.parse(input); + return value.toString(PRETTY_PRINT); + } catch (RuntimeException e) { + Output.outputError("Exception beautifying JSON: " + e.getMessage()); + return input; + } + } + + public static CustomJWToken getTokenFromView(JWTInterceptTab jwtST) { + String header = jwtST.getJwtHeaderArea().getText(); + String payload = jwtST.getJwtPayloadArea().getText(); + String signature = jwtST.getJwtSignatureArea().getText(); + return new CustomJWToken(header, payload, signature); + } + + public static class InvalidTokenFormat extends Exception { + + private static final long serialVersionUID = 1L; + + public InvalidTokenFormat(String message) { + super(message); + } + } +} diff --git a/src/app/helpers/Config.java b/src/main/java/app/helpers/Config.java similarity index 86% rename from src/app/helpers/Config.java rename to src/main/java/app/helpers/Config.java index 6cd0d05..584f17d 100644 --- a/src/app/helpers/Config.java +++ b/src/main/java/app/helpers/Config.java @@ -1,178 +1,167 @@ -package app.helpers; - -import java.io.File; -import java.io.IOException; -import java.io.PrintWriter; -import java.nio.file.Files; -import java.nio.file.Paths; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.List; - -import com.eclipsesource.json.Json; -import com.eclipsesource.json.JsonArray; -import com.eclipsesource.json.JsonObject; -import com.eclipsesource.json.JsonValue; -import com.eclipsesource.json.WriterConfig; - -public class Config { - - public static List jwtKeywords = Arrays.asList("Authorization: Bearer", "Authorization: bearer", - "authorization: Bearer", "authorization: bearer"); - public static List tokenKeywords = Arrays.asList("id_token", "ID_TOKEN", "access_token", "token"); - public static String highlightColor = "blue"; - public static String interceptComment = "Contains a JWT"; - public static boolean resetEditor = true; - public static boolean o365Support = true; - public static String configName = "config.json"; - public static String configFolderName = ".JWT4B"; - public static String configPath = - System.getProperty("user.home") + File.separator + configFolderName + File.separator + configName; - -/* - ssh-keygen -t rsa -b 2048 -m PEM -f jwtRS256.key - # Don't add passphrase - openssl rsa -in jwtRS256.key -pubout -outform PEM -out jwtRS256.key.pub - cat jwtRS256.key - cat jwtRS256.key.pub - */ - - public static String cveAttackModePublicKey = - "-----BEGIN PUBLIC KEY-----\n" + "MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAuvBC2RJqGAbPg6HoJaOl\n" - + "T6L4tMwMzGUI8TptoBlStWe+TfRcuPVfxI1U6g87/7B62768kuU55H8bd3Yd7nBm\n" - + "mdzuNthAdPDMXlrnIbOywG52iPtHAV1U5Vk5QGuj39aSuLjpBSC4jUJPcdJENpmE\n" - + "CVX+EeNwZlOEDfbtnpOTMRr/24r1CLSMwp9gtaLnE6NJzh+ycTDgyrWK9OtNA+Uq\n" - + "zwfNJ9BfE53u9JHJP/nWZopqlNQ26fgPASu8FULa8bmJ3kc0SZFCNvXyjZn7HVCw\n" - + "Ino/ZEq7oN9tphmAPBwdfQhb2xmD3gYeWrXNP/M+SKisaX1CVwaPPowjCQMbsmfC\n" + "2wIDAQAB\n" - + "-----END PUBLIC KEY-----"; - - public static String cveAttackModePrivateKey = - "-----BEGIN RSA PRIVATE KEY-----\n" + "MIIEowIBAAKCAQEAuvBC2RJqGAbPg6HoJaOlT6L4tMwMzGUI8TptoBlStWe+TfRc\n" - + "uPVfxI1U6g87/7B62768kuU55H8bd3Yd7nBmmdzuNthAdPDMXlrnIbOywG52iPtH\n" - + "AV1U5Vk5QGuj39aSuLjpBSC4jUJPcdJENpmECVX+EeNwZlOEDfbtnpOTMRr/24r1\n" - + "CLSMwp9gtaLnE6NJzh+ycTDgyrWK9OtNA+UqzwfNJ9BfE53u9JHJP/nWZopqlNQ2\n" - + "6fgPASu8FULa8bmJ3kc0SZFCNvXyjZn7HVCwIno/ZEq7oN9tphmAPBwdfQhb2xmD\n" - + "3gYeWrXNP/M+SKisaX1CVwaPPowjCQMbsmfC2wIDAQABAoIBAGtODOEzq8i86BMk\n" - + "NfCdHgA3iVGmq1YMTPTDWDgFMS/GLDvtH+hfmShnBC4SrpsXv34x32bmw7OArtCE\n" - + "8atzw8FgSzEaMu2tZ3Jl9bSnxNymy83XhyumWlwIOk/bOcb8EV6NbdyuqqETRi0M\n" - + "yHEa7+q3/M5h4pwqJmwpqL5U8bHGVGXNEbiA/TneNyXjSn03uPYaKTw4R9EG951A\n" - + "pCJf4Atba5VIfdZ59fx/6rxCuKjWlvZrklE3Cll/+A0dRN5vBSR+EBYgfedMPepM\n" - + "6TYDOsQnsy1bFJjy+aE/kwYGgtjuHOlvCpwq90SY3WueXClDfioaJ/1S6QT3q8hf\n" - + "UHodWxkCgYEA8X6+dybVvBgawxyYZEi1P/KNWC9tr2zdztnkDB4nn97UIJzxmjTh\n" - + "s81EsX0Mt24DJg36HoX5x1lDHNrR2RvIEPy8vfzTdNVa6KP7E7CWUUcW39nmt/z7\n" - + "ezlyZa8TVPBE/xvozdZuTAzd0rafUX3Ugqzn17MBshz07/K4Z0iy/C0CgYEAxiqm\n" - + "J7ul9CmNVvCnQ19tvcO7kY8h9AYIEtrqf9ubiq9W7Ldf9mXIhlG3wr6U3dXuAVVa\n" - + "4g9zkXr+N7BE4hlQcJpBn5ywtYfqzK1GRy+rfwPgC/JbWEnNDP8oYnZ8R6pkhyOC\n" - + "zqDqCZPtnmD9Je/ifdmgIkkxQD25ktyCYMhPuCcCgYEAh/MQCkfEfxUay8gnSh1c\n" - + "W9mSFJjuqJki7TXgmanIKMnqpUl1AZjPjsb56uk45XJ7N0sbCV/m04C+tVnCVPS8\n" - + "1kNRhar054rMmLbnu5fnp23bxL0Ik39Jm38llXTP7zsrvGnbzzTt9sYvglXorpml\n" - + "rsLj6ZwOUlTW1tXPVeWpTSkCgYBfAkGpWRlGx8lA/p5i+dTGn5pFPmeb9GxYheba\n" - + "KDMZudkmIwD6RHBwnatJzk/XT+MNdpvdOGVDQcGyd2t/L33Wjs6ZtOkwD5suSIEi\n" - + "TiOeAQChGbBb0v5hldAJ7R7GyVXrSMZFRPcQYoERZxTX5HwltHpHFepsD2vykpBb\n" - + "0I4QDwKBgDRH3RjKJduH2WvHOmQmXqWwtkY7zkLwSysWTW5KvCEUI+4VHMggaQ9Z\n" - + "YUXuHa8osFZ8ruJzSd0HTrDVuNTb8Q7XADOn4a5AGHu1Bhw996uNCP075dx8IOsl\n" - + "B6zvMHB8rRW93GfFd08REpsgqSm+AL6iLlZHowC00FFPtLs9e7ci\n" + "-----END RSA PRIVATE KEY-----"; - - - public static void loadConfig() { - - File configFile = new File(configPath); - - if (!configFile.getParentFile().exists()) { - Output.output("Config file directory '" + configFolderName + "' does not exist - creating it"); - boolean mkdir = configFile.getParentFile().mkdir(); - if (!mkdir) { - Output.outputError("Could not create directory '" + configFile.getParentFile().toString() + "'"); - } - } - - if (!configFile.exists()) { - Output.output("Config file '" + configPath + "' does not exist - creating it"); - try { - boolean configFileCreated = configFile.createNewFile(); - if (!configFileCreated) { - throw new IOException("Create new file failed for config file"); - } - } catch (IOException e) { - Output.outputError( - "Error creating config file '" + configPath + "' - message:" + e.getMessage() + " - cause:" + e.getCause() - .toString()); - return; - } - String defaultConfigJSONRaw = generateDefaultConfigFile(); - try { - Files.write(Paths.get(configPath), defaultConfigJSONRaw.getBytes()); - } catch (IOException e) { - Output.outputError( - "Error writing config file '" + configPath + "' - message:" + e.getMessage() + " - cause:" + e.getCause() - .toString()); - } - } - - try { - String configRaw = new String(Files.readAllBytes(Paths.get(configPath))); - JsonObject configJO = Json.parse(configRaw).asObject(); - - JsonArray jwtKeywordsJA = configJO.get("jwtKeywords").asArray(); - jwtKeywords = new ArrayList<>(); - for (JsonValue jwtKeyword : jwtKeywordsJA) { - jwtKeywords.add(jwtKeyword.asString()); - } - - JsonArray tokenKeywordsJA = configJO.get("tokenKeywords").asArray(); - tokenKeywords = new ArrayList<>(); - for (JsonValue tokenKeyword : tokenKeywordsJA) { - tokenKeywords.add(tokenKeyword.asString()); - } - - resetEditor = configJO.getBoolean("resetEditor", true); - - o365Support = configJO.getBoolean("o365Support", true); - - highlightColor = configJO.get("highlightColor").asString(); - // red, orange, yellow, green, cyan, blue, pink, magenta, gray,or a null String to clear any existing highlight. - ArrayList allowedColors = new ArrayList<>( - Arrays.asList("red", "orange", "yellow", "green", "cyan", "blue", "pink", "magenta", "gray", "none")); - if (!allowedColors.contains(highlightColor)) { - highlightColor = "none"; - Output.output( - "Unknown color, only 'red, orange, yellow, green, cyan, blue, pink, magenta, gray, none' is possible - defaulting to none."); - } - - interceptComment = configJO.get("interceptComment").asString(); - cveAttackModePublicKey = configJO.get("cveAttackModePublicKey").asString(); - cveAttackModePrivateKey = configJO.get("cveAttackModePrivateKey").asString(); - - } catch (IOException e) { - Output.outputError( - "Error loading config file '" + configPath + "' - message:" + e.getMessage() + " - cause:" + e.getCause() - .toString()); - } - } - - private static String generateDefaultConfigFile() { - JsonObject configJO = new JsonObject(); - - JsonArray jwtKeywordsJA = new JsonArray(); - for (String jwtKeyword : jwtKeywords) { - jwtKeywordsJA.add(jwtKeyword); - } - - JsonArray tokenKeywordsJA = new JsonArray(); - for (String tokenKeyword : tokenKeywords) { - tokenKeywordsJA.add(tokenKeyword); - } - - configJO.add("resetEditor", true); - configJO.add("o365Support", true); - configJO.add("highlightColor", highlightColor); - configJO.add("interceptComment", interceptComment); - configJO.add("jwtKeywords", jwtKeywordsJA); - configJO.add("tokenKeywords", tokenKeywordsJA); - - configJO.add("cveAttackModePublicKey", cveAttackModePublicKey); - configJO.add("cveAttackModePrivateKey", cveAttackModePrivateKey); - - return configJO.toString(WriterConfig.PRETTY_PRINT); - } -} +package app.helpers; + +import java.io.File; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Paths; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +import com.eclipsesource.json.Json; +import com.eclipsesource.json.JsonArray; +import com.eclipsesource.json.JsonObject; +import com.eclipsesource.json.JsonValue; +import com.eclipsesource.json.WriterConfig; + +public class Config { + + public static List tokenKeywords = Arrays.asList("id_token", "ID_TOKEN", "access_token", "token"); + public static String highlightColor = "Blue"; + public static String interceptComment = "Contains a JWT"; + public static boolean resetEditor = true; + public static boolean o365Support = true; + public static String configName = "config.json"; + public static String configFolderName = ".JWT4B"; + public static String configPath = + System.getProperty("user.home") + File.separator + configFolderName + File.separator + configName; + +/* + ssh-keygen -t rsa -b 2048 -m PEM -f jwtRS256.key + # Don't add passphrase + openssl rsa -in jwtRS256.key -pubout -outform PEM -out jwtRS256.key.pub + cat jwtRS256.key + cat jwtRS256.key.pub + */ + + public static String cveAttackModePublicKey = + "-----BEGIN PUBLIC KEY-----\n" + "MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAuvBC2RJqGAbPg6HoJaOl\n" + + "T6L4tMwMzGUI8TptoBlStWe+TfRcuPVfxI1U6g87/7B62768kuU55H8bd3Yd7nBm\n" + + "mdzuNthAdPDMXlrnIbOywG52iPtHAV1U5Vk5QGuj39aSuLjpBSC4jUJPcdJENpmE\n" + + "CVX+EeNwZlOEDfbtnpOTMRr/24r1CLSMwp9gtaLnE6NJzh+ycTDgyrWK9OtNA+Uq\n" + + "zwfNJ9BfE53u9JHJP/nWZopqlNQ26fgPASu8FULa8bmJ3kc0SZFCNvXyjZn7HVCw\n" + + "Ino/ZEq7oN9tphmAPBwdfQhb2xmD3gYeWrXNP/M+SKisaX1CVwaPPowjCQMbsmfC\n" + "2wIDAQAB\n" + + "-----END PUBLIC KEY-----"; + + public static String cveAttackModePrivateKey = + "-----BEGIN RSA PRIVATE KEY-----\n" + "MIIEowIBAAKCAQEAuvBC2RJqGAbPg6HoJaOlT6L4tMwMzGUI8TptoBlStWe+TfRc\n" + + "uPVfxI1U6g87/7B62768kuU55H8bd3Yd7nBmmdzuNthAdPDMXlrnIbOywG52iPtH\n" + + "AV1U5Vk5QGuj39aSuLjpBSC4jUJPcdJENpmECVX+EeNwZlOEDfbtnpOTMRr/24r1\n" + + "CLSMwp9gtaLnE6NJzh+ycTDgyrWK9OtNA+UqzwfNJ9BfE53u9JHJP/nWZopqlNQ2\n" + + "6fgPASu8FULa8bmJ3kc0SZFCNvXyjZn7HVCwIno/ZEq7oN9tphmAPBwdfQhb2xmD\n" + + "3gYeWrXNP/M+SKisaX1CVwaPPowjCQMbsmfC2wIDAQABAoIBAGtODOEzq8i86BMk\n" + + "NfCdHgA3iVGmq1YMTPTDWDgFMS/GLDvtH+hfmShnBC4SrpsXv34x32bmw7OArtCE\n" + + "8atzw8FgSzEaMu2tZ3Jl9bSnxNymy83XhyumWlwIOk/bOcb8EV6NbdyuqqETRi0M\n" + + "yHEa7+q3/M5h4pwqJmwpqL5U8bHGVGXNEbiA/TneNyXjSn03uPYaKTw4R9EG951A\n" + + "pCJf4Atba5VIfdZ59fx/6rxCuKjWlvZrklE3Cll/+A0dRN5vBSR+EBYgfedMPepM\n" + + "6TYDOsQnsy1bFJjy+aE/kwYGgtjuHOlvCpwq90SY3WueXClDfioaJ/1S6QT3q8hf\n" + + "UHodWxkCgYEA8X6+dybVvBgawxyYZEi1P/KNWC9tr2zdztnkDB4nn97UIJzxmjTh\n" + + "s81EsX0Mt24DJg36HoX5x1lDHNrR2RvIEPy8vfzTdNVa6KP7E7CWUUcW39nmt/z7\n" + + "ezlyZa8TVPBE/xvozdZuTAzd0rafUX3Ugqzn17MBshz07/K4Z0iy/C0CgYEAxiqm\n" + + "J7ul9CmNVvCnQ19tvcO7kY8h9AYIEtrqf9ubiq9W7Ldf9mXIhlG3wr6U3dXuAVVa\n" + + "4g9zkXr+N7BE4hlQcJpBn5ywtYfqzK1GRy+rfwPgC/JbWEnNDP8oYnZ8R6pkhyOC\n" + + "zqDqCZPtnmD9Je/ifdmgIkkxQD25ktyCYMhPuCcCgYEAh/MQCkfEfxUay8gnSh1c\n" + + "W9mSFJjuqJki7TXgmanIKMnqpUl1AZjPjsb56uk45XJ7N0sbCV/m04C+tVnCVPS8\n" + + "1kNRhar054rMmLbnu5fnp23bxL0Ik39Jm38llXTP7zsrvGnbzzTt9sYvglXorpml\n" + + "rsLj6ZwOUlTW1tXPVeWpTSkCgYBfAkGpWRlGx8lA/p5i+dTGn5pFPmeb9GxYheba\n" + + "KDMZudkmIwD6RHBwnatJzk/XT+MNdpvdOGVDQcGyd2t/L33Wjs6ZtOkwD5suSIEi\n" + + "TiOeAQChGbBb0v5hldAJ7R7GyVXrSMZFRPcQYoERZxTX5HwltHpHFepsD2vykpBb\n" + + "0I4QDwKBgDRH3RjKJduH2WvHOmQmXqWwtkY7zkLwSysWTW5KvCEUI+4VHMggaQ9Z\n" + + "YUXuHa8osFZ8ruJzSd0HTrDVuNTb8Q7XADOn4a5AGHu1Bhw996uNCP075dx8IOsl\n" + + "B6zvMHB8rRW93GfFd08REpsgqSm+AL6iLlZHowC00FFPtLs9e7ci\n" + "-----END RSA PRIVATE KEY-----"; + + + public static void loadConfig() { + + File configFile = new File(configPath); + + if (!configFile.getParentFile().exists()) { + Output.output("Config file directory '" + configFolderName + "' does not exist - creating it"); + boolean mkdir = configFile.getParentFile().mkdir(); + if (!mkdir) { + Output.outputError("Could not create directory '" + configFile.getParentFile().toString() + "'"); + } + } + + if (!configFile.exists()) { + Output.output("Config file '" + configPath + "' does not exist - creating it"); + try { + boolean configFileCreated = configFile.createNewFile(); + if (!configFileCreated) { + throw new IOException("Create new file failed for config file"); + } + } catch (IOException e) { + Output.outputError( + "Error creating config file '" + configPath + "' - message:" + e.getMessage() + " - cause:" + e.getCause() + .toString()); + return; + } + String defaultConfigJSONRaw = generateDefaultConfigFile(); + try { + Files.write(Paths.get(configPath), defaultConfigJSONRaw.getBytes()); + } catch (IOException e) { + Output.outputError( + "Error writing config file '" + configPath + "' - message:" + e.getMessage() + " - cause:" + e.getCause() + .toString()); + } + } + + try { + String configRaw = new String(Files.readAllBytes(Paths.get(configPath))); + JsonObject configJO = Json.parse(configRaw).asObject(); + + JsonArray tokenKeywordsJA = configJO.get("tokenKeywords").asArray(); + tokenKeywords = new ArrayList<>(); + for (JsonValue tokenKeyword : tokenKeywordsJA) { + tokenKeywords.add(tokenKeyword.asString()); + } + + resetEditor = configJO.getBoolean("resetEditor", true); + + o365Support = configJO.getBoolean("o365Support", true); + + highlightColor = configJO.get("highlightColor").asString(); + + // support color names regardless of case + highlightColor = highlightColor.substring(0, 1).toUpperCase() + highlightColor.substring(1).toLowerCase(); + + // red, orange, yellow, green, cyan, blue, pink, magenta, gray,or a null String to clear any existing highlight. + ArrayList allowedColors = new ArrayList<>( + Arrays.asList("Red", "Orange", "Yellow", "Green", "Cyan", "Blue", "Pink", "Magenta", "Gray", "None")); + if (!allowedColors.contains(highlightColor)) { + highlightColor = "None"; + Output.output( + "Unknown color, only 'Red, Orange, Yellow, Green, Cyan, Blue, Pink, Magenta, Gray, None' is possible - defaulting to None."); + } + + interceptComment = configJO.get("interceptComment").asString(); + cveAttackModePublicKey = configJO.get("cveAttackModePublicKey").asString(); + cveAttackModePrivateKey = configJO.get("cveAttackModePrivateKey").asString(); + + } catch (IOException e) { + Output.outputError( + "Error loading config file '" + configPath + "' - message:" + e.getMessage() + " - cause:" + e.getCause() + .toString()); + } + } + + private static String generateDefaultConfigFile() { + JsonObject configJO = new JsonObject(); + + JsonArray tokenKeywordsJA = new JsonArray(); + for (String tokenKeyword : tokenKeywords) { + tokenKeywordsJA.add(tokenKeyword); + } + + configJO.add("resetEditor", true); + configJO.add("o365Support", true); + configJO.add("highlightColor", highlightColor); + configJO.add("interceptComment", interceptComment); + configJO.add("tokenKeywords", tokenKeywordsJA); + + configJO.add("cveAttackModePublicKey", cveAttackModePublicKey); + configJO.add("cveAttackModePrivateKey", cveAttackModePrivateKey); + + return configJO.toString(WriterConfig.PRETTY_PRINT); + } +} diff --git a/src/main/java/app/helpers/CookieFlagWrapper.java b/src/main/java/app/helpers/CookieFlagWrapper.java new file mode 100644 index 0000000..7d065c5 --- /dev/null +++ b/src/main/java/app/helpers/CookieFlagWrapper.java @@ -0,0 +1,52 @@ +package app.helpers; + +public class CookieFlagWrapper { + + private final boolean secureFlag; + private final boolean httpOnlyFlag; + private final boolean isCookie; + + public CookieFlagWrapper(boolean isCookie, boolean secureFlag, boolean httpOnlyFlag) { + this.isCookie = isCookie; + this.secureFlag = secureFlag; + this.httpOnlyFlag = httpOnlyFlag; + } + + public boolean isCookie() { + return isCookie; + } + + public boolean hasHttpOnlyFlag() { + if (isCookie) { + return httpOnlyFlag; + } + return false; + } + + public boolean hasSecureFlag() { + if (isCookie) { + return secureFlag; + } + return false; + } + + public String toHTMLString() { + if (!isCookie) { + return ""; + } + String returnString = "
"; + if (!hasSecureFlag()) { + returnString += "No secure flag set. Token may be transmitted by HTTP.
"; + } else { + returnString += "Secure Flag set.
"; + } + if (!hasHttpOnlyFlag()) { + returnString += "No HttpOnly flag set. Token may accessed by JavaScript (XSS)."; + } else { + returnString += "HttpOnly Flag set."; + } + returnString += "
"; + return returnString; + } + +} diff --git a/src/app/helpers/DelayedDocumentListener.java b/src/main/java/app/helpers/DelayedDocumentListener.java similarity index 100% rename from src/app/helpers/DelayedDocumentListener.java rename to src/main/java/app/helpers/DelayedDocumentListener.java diff --git a/src/main/java/app/helpers/KeyHelper.java b/src/main/java/app/helpers/KeyHelper.java new file mode 100644 index 0000000..2ea6a63 --- /dev/null +++ b/src/main/java/app/helpers/KeyHelper.java @@ -0,0 +1,137 @@ +package app.helpers; + +import static org.apache.commons.lang.StringUtils.isNotEmpty; + +import java.security.Key; +import java.security.KeyFactory; +import java.security.KeyPair; +import java.security.KeyPairGenerator; +import java.security.NoSuchAlgorithmException; +import java.security.PrivateKey; +import java.security.PublicKey; +import java.security.interfaces.RSAPublicKey; +import java.security.spec.EncodedKeySpec; +import java.security.spec.PKCS8EncodedKeySpec; +import java.security.spec.X509EncodedKeySpec; + +import javax.crypto.Mac; +import javax.crypto.spec.SecretKeySpec; + +import org.apache.commons.codec.binary.Base64; +import org.apache.commons.lang.RandomStringUtils; + +import app.algorithm.AlgorithmType; +import app.algorithm.AlgorithmWrapper; + +public class KeyHelper { + + KeyHelper() { + + } + + private static final String[] KEY_BEGIN_MARKERS = new String[] { "-----BEGIN PUBLIC KEY-----", "-----BEGIN CERTIFICATE-----" }; + private static final String[] KEY_END_MARKERS = new String[] { "-----END PUBLIC KEY-----", "-----END CERTIFICATE-----" }; + public static final String HMAC_SHA_256 = "HmacSHA256"; + + public static String getRandomKey(String algorithm) { + AlgorithmType algorithmType = AlgorithmWrapper.getTypeOf(algorithm); + + if (algorithmType.equals(AlgorithmType.SYMMETRIC)) { + return RandomStringUtils.randomAlphanumeric(6); + } + if (algorithmType.equals(AlgorithmType.ASYMMETRIC) && algorithm.startsWith("RS")) { + try { + KeyPair keyPair = KeyPairGenerator.getInstance("RSA").generateKeyPair(); + + PublicKeyBroker.publicKey = Base64.encodeBase64String(keyPair.getPublic().getEncoded()); + return Base64.encodeBase64String(keyPair.getPrivate().getEncoded()); + } catch (NoSuchAlgorithmException e) { + Output.outputError(e.getMessage()); + } + } + if (algorithmType.equals(AlgorithmType.ASYMMETRIC) && algorithm.startsWith("ES")) { + try { + KeyPair keyPair = KeyPairGenerator.getInstance("EC").generateKeyPair(); + return Base64.encodeBase64String(keyPair.getPrivate().getEncoded()); + } catch (NoSuchAlgorithmException e) { + Output.outputError(e.getMessage()); + } + } + throw new IllegalArgumentException("Cannot get random key of provided algorithm as it does not seem valid HS, RS or ES"); + } + + public static PrivateKey generatePrivateKeyFromString(String key, String algorithm) { + PrivateKey privateKey = null; + if (isNotEmpty(key)) { + key = cleanKey(key); + try { + byte[] keyByteArray = Base64.decodeBase64(key); + KeyFactory kf = KeyFactory.getInstance(algorithm); + EncodedKeySpec keySpec = new PKCS8EncodedKeySpec(keyByteArray); + privateKey = kf.generatePrivate(keySpec); + } catch (Exception e) { + Output.outputError("Error generating private key with input string '" + key + "' and algorithm '" + algorithm + "' - " + e.getMessage() + " - "); + } + } + return privateKey; + } + + public static String cleanKey(String key) { + for (String keyBeginMarker : KEY_BEGIN_MARKERS) { + key = key.replace(keyBeginMarker, ""); + } + for (String keyEndMarker : KEY_END_MARKERS) { + key = key.replace(keyEndMarker, ""); + } + key = key.replaceAll("\\s+", "").replaceAll("\\r+", "").replaceAll("\\n+", ""); + + return key; + } + + public static RSAPublicKey loadCVEAttackPublicKey() { + String publicPEM = KeyHelper.cleanKey(Config.cveAttackModePublicKey); + KeyFactory kf; + try { + kf = KeyFactory.getInstance("RSA"); + X509EncodedKeySpec keySpecX509 = new X509EncodedKeySpec(java.util.Base64.getDecoder().decode(publicPEM)); + return (RSAPublicKey) kf.generatePublic(keySpecX509); + } catch (Exception e) { + Output.outputError("Could not load public key - " + e.getMessage()); + e.printStackTrace(); + } + return null; + } + + private static PublicKey generatePublicKeyFromString(String key, String algorithm) { + PublicKey publicKey = null; + if (isNotEmpty(key)) { + key = cleanKey(key); + byte[] keyByteArray = java.util.Base64.getDecoder().decode(key); + try { + KeyFactory kf = KeyFactory.getInstance(algorithm); + EncodedKeySpec keySpec = new X509EncodedKeySpec(keyByteArray); + publicKey = kf.generatePublic(keySpec); + } catch (Exception e) { + Output.outputError(e.getMessage()); + } + } + return publicKey; + } + + public static Key getKeyInstance(String key, String algorithm, boolean isPrivate) { + return isPrivate ? generatePrivateKeyFromString(key, algorithm) : generatePublicKeyFromString(key, algorithm); + } + + public static byte[] calcHmacSha256(byte[] secretKey, byte[] message) { + byte[] hmacSha256 = null; + try { + Mac mac = Mac.getInstance(HMAC_SHA_256); + SecretKeySpec secretKeySpec = new SecretKeySpec(secretKey, HMAC_SHA_256); + mac.init(secretKeySpec); + hmacSha256 = mac.doFinal(message); + } catch (Exception e) { + Output.outputError("Exception during " + HMAC_SHA_256 + ": " + e.getMessage()); + } + return hmacSha256; + } +} diff --git a/src/main/java/app/helpers/KeyValuePair.java b/src/main/java/app/helpers/KeyValuePair.java new file mode 100644 index 0000000..a539857 --- /dev/null +++ b/src/main/java/app/helpers/KeyValuePair.java @@ -0,0 +1,28 @@ +package app.helpers; + +public class KeyValuePair { + + private String name; + private String value; + + public KeyValuePair(String name, String value) { + this.setName(name); + this.setValue(value); + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public String getValue() { + return value; + } + + public void setValue(String value) { + this.value = value; + } +} diff --git a/src/app/helpers/Minify.java b/src/main/java/app/helpers/Minify.java similarity index 97% rename from src/app/helpers/Minify.java rename to src/main/java/app/helpers/Minify.java index 42cad35..1edaf42 100644 --- a/src/app/helpers/Minify.java +++ b/src/main/java/app/helpers/Minify.java @@ -1,574 +1,574 @@ -package app.helpers; - -import java.io.ByteArrayInputStream; -import java.io.ByteArrayOutputStream; -import java.io.IOException; -import java.io.InputStream; -import java.io.OutputStream; -import java.io.PushbackInputStream; -import java.nio.charset.StandardCharsets; - -/** - * @author Andrea Di Cesare - * - * ---------------------- Minify.java 2015-10-04 ---------------------- - * - * Copyright (c) 2015 Charles Bihis (www.whoischarles.com) - * - * This work is an adaptation of JSMin.java published by John Reilly which is a - * translation from C to Java of jsmin.c published by Douglas Crockford. - * Permission is hereby granted to use this Java version under the same - * conditions as the original jsmin.c on which all of these derivatives are - * based. - * - * - * - * --------------------- JSMin.java 2006-02-13 --------------------- - * - * Copyright (c) 2006 John Reilly (www.inconspicuous.org) - * - * This work is a translation from C to Java of jsmin.c published by Douglas - * Crockford. Permission is hereby granted to use the Java version under the - * same conditions as the jsmin.c on which it is based. - * - * - * - * ------------------ jsmin.c 2003-04-21 ------------------ - * - * Copyright (c) 2002 Douglas Crockford (www.crockford.com) - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * The Software shall be used for Good, not Evil. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE - * SOFTWARE. - * - * ---------------------- Minify.java 2015-10-04 ---------------------- - * - * Copyright (c) 2015 Charles Bihis (www.whoischarles.com) - * - * This work is an adaptation of JSMin.java published by John Reilly which is a - * translation from C to Java of jsmin.c published by Douglas Crockford. - * Permission is hereby granted to use this Java version under the same - * conditions as the original jsmin.c on which all of these derivatives are - * based. - * - * - * - * --------------------- JSMin.java 2006-02-13 --------------------- - * - * Copyright (c) 2006 John Reilly (www.inconspicuous.org) - * - * This work is a translation from C to Java of jsmin.c published by Douglas - * Crockford. Permission is hereby granted to use the Java version under the - * same conditions as the jsmin.c on which it is based. - * - * - * - * ------------------ jsmin.c 2003-04-21 ------------------ - * - * Copyright (c) 2002 Douglas Crockford (www.crockford.com) - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * The Software shall be used for Good, not Evil. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE - * SOFTWARE. - * - * ---------------------- Minify.java 2015-10-04 ---------------------- - * - * Copyright (c) 2015 Charles Bihis (www.whoischarles.com) - * - * This work is an adaptation of JSMin.java published by John Reilly which is a - * translation from C to Java of jsmin.c published by Douglas Crockford. - * Permission is hereby granted to use this Java version under the same - * conditions as the original jsmin.c on which all of these derivatives are - * based. - * - * - * - * --------------------- JSMin.java 2006-02-13 --------------------- - * - * Copyright (c) 2006 John Reilly (www.inconspicuous.org) - * - * This work is a translation from C to Java of jsmin.c published by Douglas - * Crockford. Permission is hereby granted to use the Java version under the - * same conditions as the jsmin.c on which it is based. - * - * - * - * ------------------ jsmin.c 2003-04-21 ------------------ - * - * Copyright (c) 2002 Douglas Crockford (www.crockford.com) - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * The Software shall be used for Good, not Evil. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE - * SOFTWARE. - */ -/** - * ---------------------- Minify.java 2015-10-04 ---------------------- - * - * Copyright (c) 2015 Charles Bihis (www.whoischarles.com) - * - * This work is an adaptation of JSMin.java published by John Reilly which is a - * translation from C to Java of jsmin.c published by Douglas Crockford. - * Permission is hereby granted to use this Java version under the same - * conditions as the original jsmin.c on which all of these derivatives are - * based. - * - * - * - * --------------------- JSMin.java 2006-02-13 --------------------- - * - * Copyright (c) 2006 John Reilly (www.inconspicuous.org) - * - * This work is a translation from C to Java of jsmin.c published by Douglas - * Crockford. Permission is hereby granted to use the Java version under the - * same conditions as the jsmin.c on which it is based. - * - * - * - * ------------------ jsmin.c 2003-04-21 ------------------ - * - * Copyright (c) 2002 Douglas Crockford (www.crockford.com) - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * The Software shall be used for Good, not Evil. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE - * SOFTWARE. - */ - -/** - * Minify.java is written by Charles Bihis (www.whoischarles.com) and is adapted - * from JSMin.java written by John Reilly (www.inconspicuous.org) which is - * itself a translation of jsmin.c written by Douglas Crockford - * (www.crockford.com). - * - * @see http://www.unl.edu/ucomm/templatedependents/JSMin.java - * @see http://www.crockford.com/javascript/jsmin.c - */ - -public class Minify { - - private static final int EOF = -1; - - private PushbackInputStream in; - private OutputStream out; - private int currChar; - private int nextChar; - private int line; - private int column; - - public static enum Action { - OUTPUT_CURR, DELETE_CURR, DELETE_NEXT - } - - public Minify() { - this.in = null; - this.out = null; - } - - /** - * Minifies the input JSON string. - * - * Takes the input JSON string and deletes the characters which are - * insignificant to JavaScipt. Comments will be removed, tabs will be replaced - * with spaces, carriage returns will be replaced with line feeds, and most - * spaces and line feeds will be removed. The result will be returned. - * - * @param json The JSON string for which to minify - * @return A minified, yet functionally identical, version of the input JSON - * string - */ - public String minify(String json) { - InputStream in = new ByteArrayInputStream(json.getBytes(StandardCharsets.UTF_8)); - ByteArrayOutputStream out = new ByteArrayOutputStream(); - - try { - minify(in, out); - } catch (Exception e) { - return null; - } - - return out.toString().trim(); - } - - /** - * Takes an input stream to a JSON string and outputs minified JSON to the - * output stream. - * - * Takes the input JSON via the input stream and deletes the characters which - * are insignificant to JavaScript. Comments will be removed, tabs will be - * replaced with spaaces, carriage returns will be replaced with line feeds, and - * most spaces and line feeds will be removed. The result is streamed to the - * output stream. - * - * @param in The InputStream from which to get the un-minified - * JSON - * @param out The OutputStream where the resulting minified JSON - * will be streamed to - * @throws IOException - * @throws UnterminatedRegExpLiteralException - * @throws UnterminatedCommentException - * @throws UnterminatedStringLiteralException - */ - public void minify(InputStream in, OutputStream out) - throws IOException, UnterminatedRegExpLiteralException, UnterminatedCommentException, - UnterminatedStringLiteralException { - - // Initialize - this.in = new PushbackInputStream(in); - this.out = out; - this.line = 0; - this.column = 0; - currChar = '\n'; - action(Action.DELETE_NEXT); - - // Process input - while (currChar != EOF) { - switch (currChar) { - - case ' ': - if (isAlphanum(nextChar)) { - action(Action.OUTPUT_CURR); - } else { - action(Action.DELETE_CURR); - } - break; - - case '\n': - switch (nextChar) { - case '{': - case '[': - case '(': - case '+': - case '-': - action(Action.OUTPUT_CURR); - break; - case ' ': - action(Action.DELETE_NEXT); - break; - default: - if (isAlphanum(nextChar)) { - action(Action.OUTPUT_CURR); - } else { - action(Action.DELETE_CURR); - } - } - break; - - default: - switch (nextChar) { - case ' ': - if (isAlphanum(currChar)) { - action(Action.OUTPUT_CURR); - break; - } - action(Action.DELETE_NEXT); - break; - case '\n': - switch (currChar) { - case '}': - case ']': - case ')': - case '+': - case '-': - case '"': - case '\'': - action(Action.OUTPUT_CURR); - break; - default: - if (isAlphanum(currChar)) { - action(Action.OUTPUT_CURR); - } else { - action(Action.DELETE_NEXT); - } - } - break; - default: - action(Action.OUTPUT_CURR); - break; - } - } - } - out.flush(); - } - - /** - * Process the current character with an appropriate action. - * - * The action that occurs is determined by the current character. The options - * are: - * - * 1. Output currChar: output currChar, copy nextChar to currChar, get the next - * character and save it to nextChar 2. Delete currChar: copy nextChar to - * currChar, get the next character and save it to nextChar 3. Delete nextChar: - * get the next character and save it to nextChar - * - * This method essentially treats a string as a single character. Also - * recognizes regular expressions if they are preceded by '(', ',', or '='. - * - * @param action The action to perform - * @throws IOException - * @throws UnterminatedRegExpLiteralException - * @throws UnterminatedCommentException - * @throws UnterminatedStringLiteralException - */ - private void action(Action action) - throws IOException, UnterminatedRegExpLiteralException, UnterminatedCommentException, - UnterminatedStringLiteralException { - - // Process action - switch (action) { - - case OUTPUT_CURR: - out.write(currChar); - - case DELETE_CURR: - currChar = nextChar; - - if (currChar == '\'' || currChar == '"') { - for (; ; ) { - out.write(currChar); - currChar = get(); - if (currChar == nextChar) { - break; - } - if (currChar <= '\n') { - throw new UnterminatedStringLiteralException(line, column); - } - if (currChar == '\\') { - out.write(currChar); - currChar = get(); - } - } - } - - case DELETE_NEXT: - nextChar = next(); - if (nextChar == '/' && (currChar == '(' || currChar == ',' || currChar == '=' || currChar == ':')) { - out.write(currChar); - out.write(nextChar); - for (; ; ) { - currChar = get(); - if (currChar == '/') { - break; - } else if (currChar == '\\') { - out.write(currChar); - currChar = get(); - } else if (currChar <= '\n') { - throw new UnterminatedRegExpLiteralException(line, column); - } - out.write(currChar); - } - nextChar = next(); - } - } - } - - /** - * Determines whether a given character is a letter, digit, underscore, dollar - * sign, or non-ASCII character. - * - * @param c The character to compare - * @return True if the character is a letter, digit, underscore, dollar sign, or - * non-ASCII character. False otherwise. - */ - private boolean isAlphanum(int c) { - return ((c >= 'a' && c <= 'z') || (c >= '0' && c <= '9') || (c >= 'A' && c <= 'Z') || c == '_' || c == '$' - || c == '\\' || c > 126); - } - - /** - * Returns the next character from the input stream. - * - * Will pop the next character from the input stack. If the character is a - * control character, translate it to a space or line feed. - * - * @return The next character from the input stream - * @throws IOException - */ - private int get() throws IOException { - int c = in.read(); - - if (c == '\n') { - line++; - column = 0; - } else { - column++; - } - - if (c >= ' ' || c == '\n' || c == EOF) { - return c; - } - - if (c == '\r') { - column = 0; - return '\n'; - } - - return ' '; - } - - /** - * Returns the next character from the input stream without popping it from the - * stack. - * - * @return The next character from the input stream - * @throws IOException - */ - private int peek() throws IOException { - int lookaheadChar = in.read(); - in.unread(lookaheadChar); - return lookaheadChar; - } - - /** - * Get the next character from the input stream, excluding comments. - * - * Will read from the input stream via the get() method. Will - * exclude characters that are part of comments. peek() is used to - * se if a '/' is followed by a '/' or a '*' for the purpose of identifying - * comments. - * - * @return The next character from the input stream, excluding characters from - * comments - * @throws IOException - * @throws UnterminatedCommentException - */ - private int next() throws IOException, UnterminatedCommentException { - int c = get(); - - if (c == '/') { - switch (peek()) { - - case '/': - for (; ; ) { - c = get(); - if (c <= '\n') { - return c; - } - } - - case '*': - get(); - for (; ; ) { - switch (get()) { - case '*': - if (peek() == '/') { - get(); - return ' '; - } - break; - case EOF: - throw new UnterminatedCommentException(line, column); - } - } - - default: - return c; - } - - } - return c; - } - - /** - * Exception to be thrown when an unterminated comment appears in the input. - */ - public static class UnterminatedCommentException extends Exception { - - private static final long serialVersionUID = 3L; - - public UnterminatedCommentException(int line, int column) { - super("Unterminated comment at line " + line + " and column " + column); - } - } - - /** - * Exception to be thrown when an unterminated string literal appears in the - * input. - */ - public static class UnterminatedStringLiteralException extends Exception { - - private static final long serialVersionUID = 2L; - - public UnterminatedStringLiteralException(int line, int column) { - super("Unterminated string literal at line " + line + " and column " + column); - } - } - - /** - * Exception to be thrown when an unterminated regular expression literal - * appears in the input. - */ - public static class UnterminatedRegExpLiteralException extends Exception { - - private static final long serialVersionUID = 1L; - - public UnterminatedRegExpLiteralException(int line, int column) { - super("Unterminated regular expression at line " + line + " and column " + column); - } - } -} +package app.helpers; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.io.PushbackInputStream; +import java.nio.charset.StandardCharsets; + +/** + * @author Andrea Di Cesare + * + * ---------------------- Minify.java 2015-10-04 ---------------------- + * + * Copyright (c) 2015 Charles Bihis (www.whoischarles.com) + * + * This work is an adaptation of JSMin.java published by John Reilly which is a + * translation from C to Java of jsmin.c published by Douglas Crockford. + * Permission is hereby granted to use this Java version under the same + * conditions as the original jsmin.c on which all of these derivatives are + * based. + * + * + * + * --------------------- JSMin.java 2006-02-13 --------------------- + * + * Copyright (c) 2006 John Reilly (www.inconspicuous.org) + * + * This work is a translation from C to Java of jsmin.c published by Douglas + * Crockford. Permission is hereby granted to use the Java version under the + * same conditions as the jsmin.c on which it is based. + * + * + * + * ------------------ jsmin.c 2003-04-21 ------------------ + * + * Copyright (c) 2002 Douglas Crockford (www.crockford.com) + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * The Software shall be used for Good, not Evil. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + * + * ---------------------- Minify.java 2015-10-04 ---------------------- + * + * Copyright (c) 2015 Charles Bihis (www.whoischarles.com) + * + * This work is an adaptation of JSMin.java published by John Reilly which is a + * translation from C to Java of jsmin.c published by Douglas Crockford. + * Permission is hereby granted to use this Java version under the same + * conditions as the original jsmin.c on which all of these derivatives are + * based. + * + * + * + * --------------------- JSMin.java 2006-02-13 --------------------- + * + * Copyright (c) 2006 John Reilly (www.inconspicuous.org) + * + * This work is a translation from C to Java of jsmin.c published by Douglas + * Crockford. Permission is hereby granted to use the Java version under the + * same conditions as the jsmin.c on which it is based. + * + * + * + * ------------------ jsmin.c 2003-04-21 ------------------ + * + * Copyright (c) 2002 Douglas Crockford (www.crockford.com) + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * The Software shall be used for Good, not Evil. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + * + * ---------------------- Minify.java 2015-10-04 ---------------------- + * + * Copyright (c) 2015 Charles Bihis (www.whoischarles.com) + * + * This work is an adaptation of JSMin.java published by John Reilly which is a + * translation from C to Java of jsmin.c published by Douglas Crockford. + * Permission is hereby granted to use this Java version under the same + * conditions as the original jsmin.c on which all of these derivatives are + * based. + * + * + * + * --------------------- JSMin.java 2006-02-13 --------------------- + * + * Copyright (c) 2006 John Reilly (www.inconspicuous.org) + * + * This work is a translation from C to Java of jsmin.c published by Douglas + * Crockford. Permission is hereby granted to use the Java version under the + * same conditions as the jsmin.c on which it is based. + * + * + * + * ------------------ jsmin.c 2003-04-21 ------------------ + * + * Copyright (c) 2002 Douglas Crockford (www.crockford.com) + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * The Software shall be used for Good, not Evil. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ +/** + * ---------------------- Minify.java 2015-10-04 ---------------------- + * + * Copyright (c) 2015 Charles Bihis (www.whoischarles.com) + * + * This work is an adaptation of JSMin.java published by John Reilly which is a + * translation from C to Java of jsmin.c published by Douglas Crockford. + * Permission is hereby granted to use this Java version under the same + * conditions as the original jsmin.c on which all of these derivatives are + * based. + * + * + * + * --------------------- JSMin.java 2006-02-13 --------------------- + * + * Copyright (c) 2006 John Reilly (www.inconspicuous.org) + * + * This work is a translation from C to Java of jsmin.c published by Douglas + * Crockford. Permission is hereby granted to use the Java version under the + * same conditions as the jsmin.c on which it is based. + * + * + * + * ------------------ jsmin.c 2003-04-21 ------------------ + * + * Copyright (c) 2002 Douglas Crockford (www.crockford.com) + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * The Software shall be used for Good, not Evil. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +/** + * Minify.java is written by Charles Bihis (www.whoischarles.com) and is adapted + * from JSMin.java written by John Reilly (www.inconspicuous.org) which is + * itself a translation of jsmin.c written by Douglas Crockford + * (www.crockford.com). + * + * @see http://www.unl.edu/ucomm/templatedependents/JSMin.java + * @see http://www.crockford.com/javascript/jsmin.c + */ + +public class Minify { + + private static final int EOF = -1; + + private PushbackInputStream in; + private OutputStream out; + private int currChar; + private int nextChar; + private int line; + private int column; + + public static enum Action { + OUTPUT_CURR, DELETE_CURR, DELETE_NEXT + } + + public Minify() { + this.in = null; + this.out = null; + } + + /** + * Minifies the input JSON string. + * + * Takes the input JSON string and deletes the characters which are + * insignificant to JavaScipt. Comments will be removed, tabs will be replaced + * with spaces, carriage returns will be replaced with line feeds, and most + * spaces and line feeds will be removed. The result will be returned. + * + * @param json The JSON string for which to minify + * @return A minified, yet functionally identical, version of the input JSON + * string + */ + public String minify(String json) { + InputStream in = new ByteArrayInputStream(json.getBytes(StandardCharsets.UTF_8)); + ByteArrayOutputStream out = new ByteArrayOutputStream(); + + try { + minify(in, out); + } catch (Exception e) { + return null; + } + + return out.toString().trim(); + } + + /** + * Takes an input stream to a JSON string and outputs minified JSON to the + * output stream. + * + * Takes the input JSON via the input stream and deletes the characters which + * are insignificant to JavaScript. Comments will be removed, tabs will be + * replaced with spaaces, carriage returns will be replaced with line feeds, and + * most spaces and line feeds will be removed. The result is streamed to the + * output stream. + * + * @param in The InputStream from which to get the un-minified + * JSON + * @param out The OutputStream where the resulting minified JSON + * will be streamed to + * @throws IOException + * @throws UnterminatedRegExpLiteralException + * @throws UnterminatedCommentException + * @throws UnterminatedStringLiteralException + */ + public void minify(InputStream in, OutputStream out) + throws IOException, UnterminatedRegExpLiteralException, UnterminatedCommentException, + UnterminatedStringLiteralException { + + // Initialize + this.in = new PushbackInputStream(in); + this.out = out; + this.line = 0; + this.column = 0; + currChar = '\n'; + action(Action.DELETE_NEXT); + + // Process input + while (currChar != EOF) { + switch (currChar) { + + case ' ': + if (isAlphanum(nextChar)) { + action(Action.OUTPUT_CURR); + } else { + action(Action.DELETE_CURR); + } + break; + + case '\n': + switch (nextChar) { + case '{': + case '[': + case '(': + case '+': + case '-': + action(Action.OUTPUT_CURR); + break; + case ' ': + action(Action.DELETE_NEXT); + break; + default: + if (isAlphanum(nextChar)) { + action(Action.OUTPUT_CURR); + } else { + action(Action.DELETE_CURR); + } + } + break; + + default: + switch (nextChar) { + case ' ': + if (isAlphanum(currChar)) { + action(Action.OUTPUT_CURR); + break; + } + action(Action.DELETE_NEXT); + break; + case '\n': + switch (currChar) { + case '}': + case ']': + case ')': + case '+': + case '-': + case '"': + case '\'': + action(Action.OUTPUT_CURR); + break; + default: + if (isAlphanum(currChar)) { + action(Action.OUTPUT_CURR); + } else { + action(Action.DELETE_NEXT); + } + } + break; + default: + action(Action.OUTPUT_CURR); + break; + } + } + } + out.flush(); + } + + /** + * Process the current character with an appropriate action. + * + * The action that occurs is determined by the current character. The options + * are: + * + * 1. Output currChar: output currChar, copy nextChar to currChar, get the next + * character and save it to nextChar 2. Delete currChar: copy nextChar to + * currChar, get the next character and save it to nextChar 3. Delete nextChar: + * get the next character and save it to nextChar + * + * This method essentially treats a string as a single character. Also + * recognizes regular expressions if they are preceded by '(', ',', or '='. + * + * @param action The action to perform + * @throws IOException + * @throws UnterminatedRegExpLiteralException + * @throws UnterminatedCommentException + * @throws UnterminatedStringLiteralException + */ + private void action(Action action) + throws IOException, UnterminatedRegExpLiteralException, UnterminatedCommentException, + UnterminatedStringLiteralException { + + // Process action + switch (action) { + + case OUTPUT_CURR: + out.write(currChar); + + case DELETE_CURR: + currChar = nextChar; + + if (currChar == '\'' || currChar == '"') { + for (; ; ) { + out.write(currChar); + currChar = get(); + if (currChar == nextChar) { + break; + } + if (currChar <= '\n') { + throw new UnterminatedStringLiteralException(line, column); + } + if (currChar == '\\') { + out.write(currChar); + currChar = get(); + } + } + } + + case DELETE_NEXT: + nextChar = next(); + if (nextChar == '/' && (currChar == '(' || currChar == ',' || currChar == '=' || currChar == ':')) { + out.write(currChar); + out.write(nextChar); + for (; ; ) { + currChar = get(); + if (currChar == '/') { + break; + } else if (currChar == '\\') { + out.write(currChar); + currChar = get(); + } else if (currChar <= '\n') { + throw new UnterminatedRegExpLiteralException(line, column); + } + out.write(currChar); + } + nextChar = next(); + } + } + } + + /** + * Determines whether a given character is a letter, digit, underscore, dollar + * sign, or non-ASCII character. + * + * @param c The character to compare + * @return True if the character is a letter, digit, underscore, dollar sign, or + * non-ASCII character. False otherwise. + */ + private boolean isAlphanum(int c) { + return ((c >= 'a' && c <= 'z') || (c >= '0' && c <= '9') || (c >= 'A' && c <= 'Z') || c == '_' || c == '$' + || c == '\\' || c > 126); + } + + /** + * Returns the next character from the input stream. + * + * Will pop the next character from the input stack. If the character is a + * control character, translate it to a space or line feed. + * + * @return The next character from the input stream + * @throws IOException + */ + private int get() throws IOException { + int c = in.read(); + + if (c == '\n') { + line++; + column = 0; + } else { + column++; + } + + if (c >= ' ' || c == '\n' || c == EOF) { + return c; + } + + if (c == '\r') { + column = 0; + return '\n'; + } + + return ' '; + } + + /** + * Returns the next character from the input stream without popping it from the + * stack. + * + * @return The next character from the input stream + * @throws IOException + */ + private int peek() throws IOException { + int lookaheadChar = in.read(); + in.unread(lookaheadChar); + return lookaheadChar; + } + + /** + * Get the next character from the input stream, excluding comments. + * + * Will read from the input stream via the get() method. Will + * exclude characters that are part of comments. peek() is used to + * se if a '/' is followed by a '/' or a '*' for the purpose of identifying + * comments. + * + * @return The next character from the input stream, excluding characters from + * comments + * @throws IOException + * @throws UnterminatedCommentException + */ + private int next() throws IOException, UnterminatedCommentException { + int c = get(); + + if (c == '/') { + switch (peek()) { + + case '/': + for (; ; ) { + c = get(); + if (c <= '\n') { + return c; + } + } + + case '*': + get(); + for (; ; ) { + switch (get()) { + case '*': + if (peek() == '/') { + get(); + return ' '; + } + break; + case EOF: + throw new UnterminatedCommentException(line, column); + } + } + + default: + return c; + } + + } + return c; + } + + /** + * Exception to be thrown when an unterminated comment appears in the input. + */ + public static class UnterminatedCommentException extends Exception { + + private static final long serialVersionUID = 3L; + + public UnterminatedCommentException(int line, int column) { + super("Unterminated comment at line " + line + " and column " + column); + } + } + + /** + * Exception to be thrown when an unterminated string literal appears in the + * input. + */ + public static class UnterminatedStringLiteralException extends Exception { + + private static final long serialVersionUID = 2L; + + public UnterminatedStringLiteralException(int line, int column) { + super("Unterminated string literal at line " + line + " and column " + column); + } + } + + /** + * Exception to be thrown when an unterminated regular expression literal + * appears in the input. + */ + public static class UnterminatedRegExpLiteralException extends Exception { + + private static final long serialVersionUID = 1L; + + public UnterminatedRegExpLiteralException(int line, int column) { + super("Unterminated regular expression at line " + line + " and column " + column); + } + } +} diff --git a/src/main/java/app/helpers/O365.java b/src/main/java/app/helpers/O365.java new file mode 100644 index 0000000..b5956da --- /dev/null +++ b/src/main/java/app/helpers/O365.java @@ -0,0 +1,55 @@ +package app.helpers; + +import java.nio.charset.StandardCharsets; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; +import java.util.Base64; + +import org.apache.commons.codec.binary.Hex; + +import com.auth0.jwt.algorithms.Algorithm; +import com.fasterxml.jackson.databind.JsonNode; + +import app.algorithm.AlgorithmWrapper; +import model.CustomJWToken; + +public class O365 { + + O365() { + + } + + public static boolean isO365Request(CustomJWToken token, String tokenalgo) { + return token.getHeaderJsonNode().get("ctx") != null && tokenalgo.toUpperCase().contains(AlgorithmWrapper.HS256.name()); + } + + public static void handleO365(String key, CustomJWToken token) throws NoSuchAlgorithmException { + String label = "AzureAD-SecureConversation"; + String ctx = token.getHeaderJsonNode().get("ctx").asText(); + byte[] ctxbytes = Base64.getDecoder().decode(ctx); + + JsonNode kdfVer = token.getHeaderJsonNode().get("kdf_ver"); + boolean tokenCreatedWithKDFv2 = kdfVer != null && kdfVer.asInt() == 2; + if (tokenCreatedWithKDFv2) { + byte[] fullctxbytes = new byte[24 + token.getPayloadJson().replace(" ", "").replace("\n", "").length()]; + System.arraycopy(ctxbytes, 0, fullctxbytes, 0, 24); + System.arraycopy(token.getPayloadJson().replace(" ", "").replace("\n", "").getBytes(StandardCharsets.ISO_8859_1), 0, fullctxbytes, 24, + token.getPayloadJson().replace(" ", "").replace("\n", "").length()); + MessageDigest digest = MessageDigest.getInstance("SHA-256"); + ctxbytes = digest.digest(fullctxbytes); + } + + byte[] newArr = new byte[4 + label.getBytes(StandardCharsets.UTF_8).length + 1 + ctxbytes.length + 4]; + System.arraycopy(new byte[] { (byte) 0x00, 0x00, 0x00, 0x01 }, 0, newArr, 0, 4); + System.arraycopy(label.getBytes(StandardCharsets.UTF_8), 0, newArr, 4, 26); + System.arraycopy(new byte[] { (byte) 0x00 }, 0, newArr, 30, 1); + System.arraycopy(ctxbytes, 0, newArr, 31, ctxbytes.length); + System.arraycopy(new byte[] { (byte) 0x00, 0x00, 0x01, 0x00 }, 0, newArr, newArr.length - 4, 4); + byte[] keyData = key.getBytes(StandardCharsets.ISO_8859_1); + byte[] hmacSha256 = KeyHelper.calcHmacSha256(keyData, newArr); + + Algorithm algo = AlgorithmWrapper.getSignerAlgorithm(token.getAlgorithm(), hmacSha256); + Output.output("Signing with MS O365 derived key: " + Hex.encodeHexString(hmacSha256)); + token.calculateAndSetSignature(algo); + } +} diff --git a/src/main/java/app/helpers/Output.java b/src/main/java/app/helpers/Output.java new file mode 100644 index 0000000..3674b0e --- /dev/null +++ b/src/main/java/app/helpers/Output.java @@ -0,0 +1,42 @@ +package app.helpers; + +import burp.api.montoya.logging.Logging; + +import java.text.DateFormat; +import java.text.SimpleDateFormat; +import java.util.Calendar; +import java.util.Date; +import java.util.TimeZone; + +public class Output { + private static final DateFormat DATE_FORMAT = new SimpleDateFormat("HH:mm:ss.SSS"); + + private static Logging logging; + + public static void initialise(Logging _logging) { + System.out.println("init"); + logging = _logging; + } + + public static void output(String string) { + if (logging != null) { + logging.logToOutput(formatString(string)); + } else { + System.out.println(string); + } + } + + public static void outputError(String string) { + if (logging != null) { + logging.logToError(formatString(string)); + } else { + System.err.println(string); + } + } + + private static String formatString(String string) { + Date cal = Calendar.getInstance(TimeZone.getDefault()).getTime(); + return DATE_FORMAT.format(cal.getTime()) + " | " + string; + } + +} diff --git a/src/main/java/app/helpers/PublicKeyBroker.java b/src/main/java/app/helpers/PublicKeyBroker.java new file mode 100644 index 0000000..1573238 --- /dev/null +++ b/src/main/java/app/helpers/PublicKeyBroker.java @@ -0,0 +1,15 @@ +package app.helpers; + +/** + * Created by mvetsch on 24.04.2017. + */ +public class PublicKeyBroker { + + PublicKeyBroker() { + + } + + // This hack is used to get public Key from the Random Key generator to the + // controller. Just for logging + public static String publicKey; +} diff --git a/src/main/java/app/helpers/TokenChecker.java b/src/main/java/app/helpers/TokenChecker.java new file mode 100644 index 0000000..e9bdcf6 --- /dev/null +++ b/src/main/java/app/helpers/TokenChecker.java @@ -0,0 +1,43 @@ +package app.helpers; + +import org.apache.commons.lang.StringUtils; + +import com.auth0.jwt.JWT; +import com.auth0.jwt.interfaces.DecodedJWT; + +public class TokenChecker { + + private TokenChecker() { + + } + + public static final String JWT_ALLOWED_CHARS_REGEXP = "[A-Za-z0-9+/=_-]+"; + + public static boolean isValidJWT(String jwt) { + int dotCount = StringUtils.countMatches(jwt, "."); + if (dotCount != 2) { + return false; + } + + jwt = jwt.trim(); + if (StringUtils.contains(jwt, " ")) { + return false; + } + + for (String part : StringUtils.split(jwt, ".")) { + if (!part.matches(JWT_ALLOWED_CHARS_REGEXP)) { + return false; + } + } + + try { + DecodedJWT decoded = JWT.decode(jwt); + decoded.getAlgorithm(); + return true; + } catch (Exception ignored) { + // ignored + } + + return false; + } +} diff --git a/src/main/java/app/tokenposition/AuthorizationBearerHeader.java b/src/main/java/app/tokenposition/AuthorizationBearerHeader.java new file mode 100644 index 0000000..93e3b60 --- /dev/null +++ b/src/main/java/app/tokenposition/AuthorizationBearerHeader.java @@ -0,0 +1,72 @@ +package app.tokenposition; + +import java.util.List; +import java.util.Optional; + +import burp.api.montoya.http.message.HttpHeader; +import burp.api.montoya.http.message.HttpMessage; +import burp.api.montoya.http.message.requests.HttpRequest; +import burp.api.montoya.http.message.responses.HttpResponse; +import model.CustomJWToken; + +// finds and replaces JWT's in authorization headers +public class AuthorizationBearerHeader extends ITokenPosition { + + private Optional headerContainsJwt; + private String headerName; + private String headerKeyword; + + public AuthorizationBearerHeader(HttpMessage httpMessage, boolean isRequest) { + super(httpMessage, isRequest); + } + + public boolean positionFound() { + try { + for (HttpHeader header : httpMessage.headers()) { + headerContainsJwt = containsJwt(header.value(), List.of("Bearer","bearer","BEARER")); + if (headerContainsJwt.isPresent()) { + headerName = header.name(); + headerKeyword = headerContainsJwt.get(); + String jwtValue = header.value().substring(headerContainsJwt.get().length() + 1); + if (CustomJWToken.isValidJWT(jwtValue)) { + this.token = jwtValue; + return true; + } + } + } + } catch (Exception ignored) { + System.out.println(ignored.getMessage()); + } + return false; + } + + private Optional containsJwt(String header, List jwtKeywords) { + for (String keyword : jwtKeywords) { + if (header.startsWith(keyword)) { + String jwt = header.replace(keyword, "").trim(); + if (CustomJWToken.isValidJWT(jwt)) { + return Optional.of(keyword); + } + } + } + return Optional.empty(); + } + + @Override + public HttpRequest getRequest() { + HttpRequest httpRequest = HttpRequest.httpRequest(httpMessage.toString()); + if (headerContainsJwt.isEmpty()) { + return httpRequest; + } + return httpRequest.withUpdatedHeader(headerName, headerKeyword + " " + token); + } + + @Override + public HttpResponse getResponse() { + HttpResponse httpResponse = HttpResponse.httpResponse(httpMessage.toString()); + if (headerContainsJwt.isEmpty()) { + return httpResponse; + } + return httpResponse.withUpdatedHeader(headerName, headerKeyword + " " + token); + } +} diff --git a/src/main/java/app/tokenposition/Body.java b/src/main/java/app/tokenposition/Body.java new file mode 100644 index 0000000..9a5a352 --- /dev/null +++ b/src/main/java/app/tokenposition/Body.java @@ -0,0 +1,138 @@ +package app.tokenposition; + +import java.util.regex.Pattern; + +import burp.api.montoya.http.message.HttpMessage; +import burp.api.montoya.http.message.requests.HttpRequest; +import burp.api.montoya.http.message.responses.HttpResponse; +import org.apache.commons.lang.StringUtils; + +import com.eclipsesource.json.Json; +import com.eclipsesource.json.JsonObject; + +import app.helpers.KeyValuePair; +import app.helpers.TokenChecker; + +//finds and replaces JWT's in HTTP bodies +public class Body extends ITokenPosition { + + public Body(HttpMessage httpMessage, boolean isRequest) { + super(httpMessage, isRequest); + } + + @Override + public boolean positionFound() { + KeyValuePair postJWT = getJWTFromBody(); + if (postJWT != null) { + token = postJWT.getValue(); + return true; + } + return false; + } + + private KeyValuePair getJWTFromBody() { + KeyValuePair ret; + if ((ret = getJWTFromBodyWithParameters()) != null) { + return ret; + } else if ((ret = getJWTFromBodyWithJson()) != null) { + return ret; + } else { + return getJWTFromBodyWithoutParametersOrJSON(); + } + } + + private KeyValuePair getJWTFromBodyWithoutParametersOrJSON() { + String body = this.httpMessage.bodyToString(); + + String[] split = StringUtils.split(body); + for (String strg : split) { + if (TokenChecker.isValidJWT(strg)) { + return new KeyValuePair("", strg); + } + } + + return null; + } + + private KeyValuePair getJWTFromBodyWithJson() { + String body = this.httpMessage.bodyToString(); + + JsonObject obj; + try { + if (body.length() < 2) { + return null; + } + obj = Json.parse(body).asObject(); + } catch (Exception e) { + return null; + } + return lookForJwtInJsonObject(obj); + } + + private KeyValuePair lookForJwtInJsonObject(JsonObject object) { + KeyValuePair rec; + for (String name : object.names()) { + if (object.get(name).isString()) { + if (TokenChecker.isValidJWT(object.get(name).asString())) { + return new KeyValuePair(name, object.get(name).asString().trim()); + } + } else if (object.get(name).isObject()) { + if ((rec = lookForJwtInJsonObject(object.get(name).asObject())) != null) { + return rec; + } + } + } + return null; + } + + private KeyValuePair getJWTFromBodyWithParameters() { + String body = this.httpMessage.bodyToString(); + + int from = 0; + int index = body.contains("&") ? body.indexOf("&") : body.length(); + int parameterCount = StringUtils.countMatches(body, "&") + 1; + + for (int i = 0; i < parameterCount; i++) { + String parameter = body.substring(from, index); + parameter = parameter.replace("&", ""); + + String[] parameterSplit = parameter.split(Pattern.quote("=")); + if (parameterSplit.length > 1) { + String name = parameterSplit[0]; + String value = parameterSplit[1]; + if (TokenChecker.isValidJWT(value)) { + return new KeyValuePair(name, value); + } + + from = index; + index = body.indexOf("&", index + 1); + if (index == -1) { + index = body.length(); + } + } + } + + return null; + } + + @Override + public HttpRequest getRequest() { + return HttpRequest.httpRequest(replaceTokenImpl(this.token, httpMessage.toString())); + } + + @Override + public HttpResponse getResponse() { + return HttpResponse.httpResponse(replaceTokenImpl(this.token, httpMessage.toString())); + } + + private String replaceTokenImpl(String newToken, String httpMessage) { + String newMessage = httpMessage; + + KeyValuePair postJWT = getJWTFromBody(); + if (postJWT != null) { + newMessage = httpMessage.replace(postJWT.getValue(), newToken); + } + + return newMessage; + } +} diff --git a/src/main/java/app/tokenposition/Cookie.java b/src/main/java/app/tokenposition/Cookie.java new file mode 100644 index 0000000..ce27913 --- /dev/null +++ b/src/main/java/app/tokenposition/Cookie.java @@ -0,0 +1,116 @@ +package app.tokenposition; + +import java.util.List; + +import app.helpers.CookieFlagWrapper; +import app.helpers.KeyValuePair; +import app.helpers.TokenChecker; +import burp.api.montoya.http.message.HttpHeader; +import burp.api.montoya.http.message.HttpMessage; +import burp.api.montoya.http.message.params.HttpParameter; +import burp.api.montoya.http.message.requests.HttpRequest; +import burp.api.montoya.http.message.responses.HttpResponse; + +//finds and replaces JWT's in cookies +public class Cookie extends ITokenPosition { + + private static final String SET_COOKIE_HEADER = "Set-Cookie"; + private static final String COOKIE_HEADER = "Cookie"; + + private CookieFlagWrapper cFW; + private KeyValuePair cookieHeader; + + public Cookie(HttpMessage httpMessage, boolean isRequest) { + super(httpMessage, isRequest); + } + + @Override + public boolean positionFound() { + try { + List headers = this.httpMessage.headers(); + + cookieHeader = getJWTInCookieHeader(headers); + if (cookieHeader != null) { + token = cookieHeader.getValue(); + return true; + } + } catch (Exception ignored) { + // + } + + return false; + } + + @Override + public HttpRequest getRequest() { + HttpRequest httpRequest = (HttpRequest) httpMessage; + return httpRequest.withParameter(HttpParameter.cookieParameter(cookieHeader.getName(), token)); + } + + @Override + public HttpResponse getResponse() { + return HttpResponse.httpResponse(replaceTokenImpl(this.token, httpMessage.toString())); + } + + private String replaceTokenImpl(String newToken, String httpMessageAsString) { + String newMessage = httpMessageAsString; + List headers = this.httpMessage.headers(); + + KeyValuePair cookieJWT = getJWTInCookieHeader(headers); + if (cookieJWT != null) { + newMessage = httpMessageAsString.replace(cookieJWT.getValue(), newToken); + } + + return newMessage; + } + + // finds the first jwt in the set-cookie or cookie header(s) + public KeyValuePair getJWTInCookieHeader(List headers) { + cFW = new CookieFlagWrapper(false, false, false); + + for (HttpHeader httpHeader : headers) { + if (httpHeader.name().regionMatches(true, 0, SET_COOKIE_HEADER, 0, SET_COOKIE_HEADER.length())) { + String setCookieValue = httpHeader.value(); + if (setCookieValue.length() > 1 && setCookieValue.contains("=")) { // sanity check + int nameMarkerPos = setCookieValue.indexOf("="); + String name = setCookieValue.substring(0,nameMarkerPos); + String value = setCookieValue.substring(nameMarkerPos); + int flagMarker = value.indexOf(";"); + if (flagMarker != -1) { + value = value.substring(0, flagMarker); + cFW = new CookieFlagWrapper(true, setCookieValue.toLowerCase().contains("; secure"), setCookieValue.toLowerCase().contains("; httponly")); + } else { + cFW = new CookieFlagWrapper(true, false, false); + } + if (TokenChecker.isValidJWT(value)) { + return new KeyValuePair(name, value); + } + } + } + + if (httpHeader.name().regionMatches(true, 0, COOKIE_HEADER, 0, COOKIE_HEADER.length())) { + String cookieHeaderValue = httpHeader.value(); + if (cookieHeaderValue != null && !cookieHeaderValue.isEmpty()) { + String[] pairs = cookieHeaderValue.split(";\\s*"); + for (String pair : pairs) { + String[] parts = pair.split("=", 2); + if (parts.length == 2) { + String name = parts[0].trim(); + String value = parts[1].trim(); + if (TokenChecker.isValidJWT(value)) { + return new KeyValuePair(name, value); + } + } + } + } + } + } + return null; + } + + + @Override + public CookieFlagWrapper getcFW() { + return this.cFW; + } +} diff --git a/src/main/java/app/tokenposition/ITokenPosition.java b/src/main/java/app/tokenposition/ITokenPosition.java new file mode 100644 index 0000000..32f078d --- /dev/null +++ b/src/main/java/app/tokenposition/ITokenPosition.java @@ -0,0 +1,95 @@ +package app.tokenposition; + +import java.util.Arrays; +import java.util.List; + +import app.helpers.CookieFlagWrapper; +import app.helpers.Output; + +import burp.api.montoya.http.message.HttpMessage; +import burp.api.montoya.http.message.requests.HttpRequest; +import burp.api.montoya.http.message.responses.HttpResponse; +import model.Strings; + +public abstract class ITokenPosition { + + protected boolean isRequest; + protected HttpMessage httpMessage; + protected String token; + + protected ITokenPosition(HttpMessage httpMessage, boolean isRequest) { + this.httpMessage = httpMessage; + this.isRequest = isRequest; + } + + public abstract boolean positionFound(); + + public abstract HttpRequest getRequest(); + + public abstract HttpResponse getResponse(); + + private static CookieFlagWrapper cookieFlagWrap; + + public static ITokenPosition findTokenPositionImplementation(HttpMessage httpMessage, boolean isRequest) { + List> implementations = Arrays.asList(AuthorizationBearerHeader.class, PostBody.class, Body.class, Cookie.class); + + for (Class implClass : implementations) { + try { + ITokenPosition impl = (ITokenPosition) implClass.getConstructors()[0].newInstance(httpMessage, isRequest); + + if (impl.positionFound()) { + if (impl instanceof Cookie) { + cookieFlagWrap = ((Cookie) impl).getcFW(); + } else { + cookieFlagWrap = new CookieFlagWrapper(false, false, false); + } + return impl; + } + } catch (Exception e) { + // sometimes 'isEnabled' is called in order to build the views + // before an actual request / response passes through - in that case + // it is not worth reporting + if (!e.getMessage().equals("Request cannot be null") && !e.getMessage().equals("1")) { + Output.outputError(e.getMessage()); + } + return null; + } + } + return null; + } + + public String getToken() { + return (this.token != null) ? this.token : ""; + } + + public void replaceToken(String newToken) { + this.token = newToken; + } + + public void addHeader(String name, String value) { + // add header + if (isRequest) { + HttpRequest request = (HttpRequest) httpMessage; + httpMessage = request.withAddedHeader(name, value); + } else { + HttpResponse response = (HttpResponse) httpMessage; + httpMessage = response.withAddedHeader(name, value); + } + } + + public void cleanJWTHeaders() { + // remove headers that start with Strings.JWTHeaderPrefix) + if (isRequest) { + HttpRequest request = (HttpRequest) httpMessage; + httpMessage = request.withRemovedHeader(Strings.JWT_HEADER_PREFIX); + } else { + HttpResponse response = (HttpResponse) httpMessage; + httpMessage = response.withRemovedHeader(Strings.JWT_HEADER_PREFIX); + } + } + + public CookieFlagWrapper getcFW() { + return cookieFlagWrap; + } + +} diff --git a/src/main/java/app/tokenposition/PostBody.java b/src/main/java/app/tokenposition/PostBody.java new file mode 100644 index 0000000..77083c6 --- /dev/null +++ b/src/main/java/app/tokenposition/PostBody.java @@ -0,0 +1,58 @@ +package app.tokenposition; + +import java.util.List; + +import burp.api.montoya.http.message.params.HttpParameter; +import burp.api.montoya.http.message.params.HttpParameterType; +import burp.api.montoya.http.message.params.ParsedHttpParameter; + +import burp.api.montoya.http.message.HttpMessage; +import burp.api.montoya.http.message.requests.HttpRequest; +import burp.api.montoya.http.message.responses.HttpResponse; + +import app.helpers.Config; +import app.helpers.TokenChecker; + +public class PostBody extends ITokenPosition { + + private HttpParameter httpParameter; + + public PostBody(HttpMessage httpMessage, boolean isRequest) { + super(httpMessage, isRequest); + } + + @Override + public boolean positionFound() { + if (isRequest) { + httpParameter = getJWTFromPostBody(); + if (httpParameter != null) { + token = httpParameter.value(); + return true; + } + } + return false; + } + + private HttpParameter getJWTFromPostBody() { + if (isRequest) { + HttpRequest httpRequest = (HttpRequest) httpMessage; + List parsedHttpParameters = httpRequest.parameters(HttpParameterType.BODY); + + return parsedHttpParameters.stream().filter(parameter -> Config.tokenKeywords.contains(parameter.name()) && TokenChecker.isValidJWT(parameter.value())).findFirst().orElse(null); + } + + return null; + } + + @Override + public HttpRequest getRequest() { + HttpRequest httpRequest = (HttpRequest) httpMessage; + + return httpRequest.withParameter(HttpParameter.bodyParameter(httpParameter.name(), token)); + } + + @Override + public HttpResponse getResponse() { + return (HttpResponse) httpMessage; + } +} diff --git a/src/main/java/burp/JWT4BContextMenuItemsProvider.java b/src/main/java/burp/JWT4BContextMenuItemsProvider.java new file mode 100644 index 0000000..1f6e1a5 --- /dev/null +++ b/src/main/java/burp/JWT4BContextMenuItemsProvider.java @@ -0,0 +1,51 @@ +package burp; + +import burp.api.montoya.ui.contextmenu.ContextMenuEvent; +import burp.api.montoya.ui.contextmenu.ContextMenuItemsProvider; +import burp.api.montoya.http.message.HttpRequestResponse; +import burp.api.montoya.ui.contextmenu.MessageEditorHttpRequestResponse.SelectionContext; + +import app.controllers.JWTSuiteTabController; +import model.Strings; + +import javax.swing.*; +import java.awt.*; +import java.util.ArrayList; +import java.util.List; + +public class JWT4BContextMenuItemsProvider implements ContextMenuItemsProvider { + private final JWTSuiteTabController jstC; + + public JWT4BContextMenuItemsProvider(JWTSuiteTabController jstC) { + this.jstC = jstC; + } + + @Override + public List provideMenuItems(ContextMenuEvent event) { + List menuItemList = new ArrayList<>(); + + // editor and selection need to be present + if (event.messageEditorRequestResponse().isPresent() && event.messageEditorRequestResponse().get().selectionOffsets().isPresent()) { + HttpRequestResponse requestResponse = event.messageEditorRequestResponse().get().requestResponse(); + + SelectionContext selectionContext = event.messageEditorRequestResponse().get().selectionContext(); + int startIndex = event.messageEditorRequestResponse().get().selectionOffsets().get().startIndexInclusive(); + int endIndex = event.messageEditorRequestResponse().get().selectionOffsets().get().endIndexExclusive(); + + String selectedText; + + if (selectionContext == SelectionContext.REQUEST) { + selectedText = requestResponse.request().toString().substring(startIndex, endIndex); + } else { + selectedText = requestResponse.response().toString().substring(startIndex, endIndex); + } + + JMenuItem retrieveSelectedRequestItem = new JMenuItem(Strings.CONTEXT_MENU_STRING); + retrieveSelectedRequestItem.addActionListener(e -> jstC.contextActionSendJWTtoSuiteTab(selectedText, true)); + + menuItemList.add(retrieveSelectedRequestItem); + } + + return menuItemList; + } +} diff --git a/src/main/java/burp/JWT4BEditorProvider.java b/src/main/java/burp/JWT4BEditorProvider.java new file mode 100644 index 0000000..a347197 --- /dev/null +++ b/src/main/java/burp/JWT4BEditorProvider.java @@ -0,0 +1,57 @@ +package burp; + +import burp.api.montoya.ui.editor.extension.*; + +import app.controllers.JWTRequestTabController; +import app.controllers.JWTResponseTabController; +import app.controllers.JWTRequestInterceptTabController; +import app.controllers.JWTResponseInterceptTabController; +import gui.JWTInterceptTab; +import gui.JWTViewTab; +import gui.RSyntaxTextAreaFactory; +import model.JWTInterceptModel; +import model.JWTTabModel; + +public class JWT4BEditorProvider implements HttpRequestEditorProvider, HttpResponseEditorProvider { + private final RSyntaxTextAreaFactory rSyntaxTextAreaFactory; + + public JWT4BEditorProvider(RSyntaxTextAreaFactory rSyntaxTextAreaFactory) { + this.rSyntaxTextAreaFactory = rSyntaxTextAreaFactory; + } + + @Override + public ExtensionProvidedHttpRequestEditor provideHttpRequestEditor(EditorCreationContext creationContext) { + ExtensionProvidedHttpRequestEditor jwtTC; + + if (creationContext.editorMode() == EditorMode.DEFAULT) { + JWTInterceptModel jwtSTM = new JWTInterceptModel(); + JWTInterceptTab jwtST = new JWTInterceptTab(jwtSTM, rSyntaxTextAreaFactory); + jwtTC = new JWTRequestInterceptTabController(jwtSTM, jwtST); + } else { + // Read Only + JWTTabModel jwtTM = new JWTTabModel(); + JWTViewTab jwtVT = new JWTViewTab(jwtTM, rSyntaxTextAreaFactory); + jwtTC = new JWTRequestTabController(jwtTM, jwtVT); + } + + return jwtTC; + } + + @Override + public ExtensionProvidedHttpResponseEditor provideHttpResponseEditor(EditorCreationContext creationContext) { + ExtensionProvidedHttpResponseEditor jwtTC; + + if (creationContext.editorMode() == EditorMode.DEFAULT) { + JWTInterceptModel jwtSTM = new JWTInterceptModel(); + JWTInterceptTab jwtST = new JWTInterceptTab(jwtSTM, rSyntaxTextAreaFactory); + jwtTC = new JWTResponseInterceptTabController(jwtSTM, jwtST); + } else { + // Read Only + JWTTabModel jwtTM = new JWTTabModel(); + JWTViewTab jwtVT = new JWTViewTab(jwtTM, rSyntaxTextAreaFactory); + jwtTC = new JWTResponseTabController(jwtTM, jwtVT); + } + + return jwtTC; + } +} diff --git a/src/main/java/burp/JWT4BExtension.java b/src/main/java/burp/JWT4BExtension.java new file mode 100644 index 0000000..b2bae72 --- /dev/null +++ b/src/main/java/burp/JWT4BExtension.java @@ -0,0 +1,56 @@ +package burp; + +import app.controllers.JWTSuiteTabController; +import burp.api.montoya.BurpExtension; +import burp.api.montoya.MontoyaApi; +import burp.api.montoya.extension.Extension; +import burp.api.montoya.http.Http; +import burp.api.montoya.logging.Logging; +import burp.api.montoya.ui.UserInterface; + +import app.controllers.HighLightController; +import app.helpers.Config; +import app.helpers.Output; +import gui.JWTSuiteTab; +import gui.RSyntaxTextAreaFactory; +import model.JWTSuiteTabModel; +import model.Settings; + +public class JWT4BExtension implements BurpExtension { + @Override + public void initialize(MontoyaApi api) { + Extension extension = api.extension(); + UserInterface userInterface = api.userInterface(); + Logging logging = api.logging(); + Http http = api.http(); + + RSyntaxTextAreaFactory rSyntaxTextAreaFactory = new RSyntaxTextAreaFactory(userInterface); + + // Logging + Output.initialise(logging); + Output.output("JWT4B says hi!"); + + // Editor + JWT4BEditorProvider editorProvider = new JWT4BEditorProvider(rSyntaxTextAreaFactory); + userInterface.registerHttpRequestEditorProvider(editorProvider); + userInterface.registerHttpResponseEditorProvider(editorProvider); + + // Settings + Config.loadConfig(); + + // Request & Response Highlighter + final HighLightController highLightController = new HighLightController(); + http.registerHttpHandler(highLightController); + + // SuiteTab + JWTSuiteTabModel jwtSuiteTabModel = new JWTSuiteTabModel(); + JWTSuiteTab suiteTab = new JWTSuiteTab(jwtSuiteTabModel, rSyntaxTextAreaFactory); + api.userInterface().registerSuiteTab(Settings.TAB_NAME, suiteTab); + + // Context Menu + JWTSuiteTabController tabController = new JWTSuiteTabController(jwtSuiteTabModel, suiteTab); + userInterface.registerContextMenuItemsProvider(new JWT4BContextMenuItemsProvider(tabController)); + + extension.setName(Settings.EXTENSION_NAME); + } +} diff --git a/src/main/java/gui/JLabelLink.java b/src/main/java/gui/JLabelLink.java new file mode 100644 index 0000000..35773f3 --- /dev/null +++ b/src/main/java/gui/JLabelLink.java @@ -0,0 +1,106 @@ +package gui; + +import java.awt.Cursor; +import java.awt.Desktop; +import java.awt.event.MouseAdapter; +import java.awt.event.MouseEvent; +import java.awt.image.BufferedImage; +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.io.Serial; +import java.net.URI; +import java.util.Base64; +import java.util.Objects; + +import javax.imageio.ImageIO; +import javax.swing.BoxLayout; +import javax.swing.ImageIcon; +import javax.swing.JFrame; +import javax.swing.JLabel; +import javax.swing.JPanel; +import javax.swing.WindowConstants; +import javax.swing.border.EmptyBorder; + +import app.helpers.Output; + +public class JLabelLink extends JFrame { + @Serial + private static final long serialVersionUID = 1L; + + private static final String LOGO_DATA = "iVBORw0KGgoAAAANSUhEUgAAAd0AAACKCAYAAADvwZCVAAAABmJLR0QA/wD/AP+gvaeTAAAACXBIWXMAAAsTAAALEwEAmpwYAAAAB3RJTUUH4QQYDA4ppMr3sQAAAB1pVFh0Q29tbWVudAAAAAAAQ3JlYXRlZCB3aXRoIEdJTVBkLmUHAAAgAElEQVR42u2deZRddZXvP/t37q2q1JCpEhLIUBkqlTAIiIwOD31ii4227cOgYEv60W1aW6ENEen13norL93vvT8YBKUdlwqI3SIRbRrbVlkOtAOKQRlDhgqZU5nnpKZ7zn5/nFMSioTce865t+45tT9r1R+QqnPP2fd3ft/f3r/92xsMwzAMwzAMwzAMwzAMwzAMwzAMwzAMwzAMwzAMwzAMwzAMwzAMwzAMwzAMwzAMwzAMwzAMwzAMwzAMwzAMwzAMwzAMwzAMwzAMwzAMwzAMwzAMwzAMwzAMwzAMwzAMwzAMwzAMwzAMwzAMwzAMwxjNSAbvuTFn34EPlOrANgEwmFEbJnnu/lP8e0NG35NqU4rGbqZYe2Nn47xqfsCO7pKsqI1ddBkN7OsckbG5DhjY0a19E9CX9hNcsyJ7Y8FEtzwmAR8DpuboO3gc+E4kekmYCnwUmBzn/QWeA+4H+jJmv2nA3wLjY/ztAeDLwOaT/PsE4G+AGTZVvGq8PAr8KEs3vXPJvPO8gvsg4sZWyy5+KXh0ymfWVN0uKxdT7Bi34CMicvZIfP+CBgEEogwq9Ktor1OOoBxUdH8QyO5BBvYUShyU0pGjp31h91EJx82op5Cx+x0LXAfMz9kE9t0URLcNuAZYEPPvfwg8mEHRnQrcAEyJ8bdbomc+GS2RTc+3qeJVY3Y78GMyMpGuXzxnnFf0Poq4jwCuWnbxCrKtFouR01qmF8TJe0DeOTIDQEKPTULPTaI1hzgZAAY8T/s9mo5ogV0iTZv2fbq9e7fqamBNKRjcPHXzS3tllHrHhYzet4X7Xs0A0BvTNgo0VXEyqiZNgBfzuUvAsTLGmo23V4+XTDF+XMOfqsh/ExGvakbR2ptFROpnbIoUgIJAc/TKTBFhLnCZooGIHgP2FaWxe++c+U/uvoUnpM//ffuO7p7RJMAFmz9yw2AkunFpJLt7/HEXC/2cek/XyDgHPjlvTsnJIpDJZo2R0mNxIK1AK8JMVblchAPa7F7cO+vMH/UsLT12eECf7rqnO/fvo7PhYKIb0RB5jFn0dOOO46Okl8Rm1CErF1McKHp/AfLWuvIKTYQ9cdLuxL1ZHP+r6HnfHN9U+D89S+ZdrAszOQ+Z6JroVvYOAMXox0TXyA0d4+a/2Tn3FyLSaNaoVwV2DeK8TufcJ4vFwtf2zZp/86aPzzjDRNeodwYSerpe5O1mjSTh5aNgRx3yyoEl0yeCWyQw16yRCe+3IM6dg3PLWlpabtvz6c5LTXSNvHq6EO7vZ1F0zdM1XsUycH6x9WpxvBcRm+ey5fm2IO5apOG2XbcseDc5S2S0wZgfgkhE4h49Go2ie8Q83Xxy09I5ZwUifwkyzqyRSa/XAW92Ist33bLgKhNdo15JKrpZ3NNNGl42TzdnbFjU0aSu+CGBiyx5KtPCK+LcBc7J3++8ef4bTXSNekMjzy2u6GZ1T9fCy8YraJnU8HYV71oRVzRr5EJ+3+gKctOWGzunm+ga9cYR4hcuyGp4uZF4R500YWTAqEN2fXLm6eIKN4gw06yRI49X5D1NTd61eThOZKJrnm6WPV2JPN04IUQ/El0jJywDR3HMNeK40sLKuZPeMeK8D+6aM+8cE12j3jzd0bSn64hfSatkopsvPrG06w1O3PUgY8waZa7UQ/wKfgIdgXqX0SLqdR7ee3VZtispZvHm9wC76uA+2oB6ermTerpZDC97xG/rV67oHgYO1eBZWlNaBPcRntmuJkENPqMidt/S3ibiLULkPPNyK5o29mug3xORA+WJH54qDYFqsxPGKjIZ1dOBdkTaJKy/XEW9knfs3z/nPnhps4lubdgE3FgH4lAkbCf3AeqrdOJoE12XQHTLCS/vBv6RsLtVNWkHlgJdCa8zADwEfJ/q71U/Tx01PlDa/9QJV1PFhgZ5RJRdiH9n+5h1a8r9mxWrkIXgrZva2TTeG2hxXsOEwBXmOPQNqrwFcecB7WkvfkREUM4NmrwLOXk7ThPdlPGBp+vgPorA1XVon17iZ+NmcU/XI9zTrZan2w88VoPnmAb8VUrRjmeBhxlFCWLbl87qEHHXKzIlySyvqjravGQFgpLny/KKx4sP3QOEUaAeYNUy+MHHbpo92WssXibO+4AqVwLj0rSpQkug3iW6jH+T5dk8eWB7uvkiSdecJF5jXj3dLM+lo4KViyk2FBo/5MT91ySTu6r2AXtGYr8yLyyHYOrnNuycfPvafy0eOfgJCYJbFX0+ZZs6J3Lu/p45LVm1k4luvhggWRP6rLX3S7qne8SGTLaZ2TbvMtR9WBMsGFVV0eDHaPBrs2g6jPv89r0/2bj6a5SC/4nqU2kJb3h6iI5Sa8MUE12jXjzduKKb5PjNSIpu3PBy0lrVxgiz6WMzJ3ietwiRrkQhTNX1GgRfR2SHWTU9rlmBP+nONd8X9e8QdFta11VkklfQSSa6Rh483ayJbpLwch/WwD6zKMiY1jHvU5E/lyQNDVT7UX3A80o/N6umj4Ae2tX/CPAdVU0rz6DVV5loomuY6I6MpxtXdI9hJSAzS8/SzjOdyCKQCfH1VlVVf+YFwQNHBgYGzKrVYfb9m/pKg/7DaGoZx06cjtWMdh8y0c2f6CZpZD+aPF0T3Yyy9kYai65wHSKXJsuM1Z0B/r0T7ly7waxaXRr8/udBn03FexYQ1cwWQDHRNU93uKebpTGRZE/Xmh1klPbGrreJJ9eKuNhH3FS1pBp8a/DA4e+bRavP3Z/ddEjRZ1U1cStNVZVAXWaPdpno5tPTjZMpmNVEqrgTr3m6GWTHTbOnBM7dgMrsBJO2gj7pB3rftK/0HDOrVp/lEAjBRpFETsHQVKVO/cxuB5jo5osgEpMknm6WRLeB+AVeTHQzxjJw0tS4UETelTCsfFACvX/KHWufN6vWDt93+1VTeecUOCwZPY9uopsvktZfzproxm3rBxZezhwfv2Xu+R5yPUjswgiqGkgQPNLnH/yuWFvHGouNDpKOUParuANZtUPBhkLuOMzoCS/H3YNW83Szxc6/ndwqFBch8vqEZQVf9IPg69M+07PHrFpj0XUp5Ywo+z3VvSa6Rj15unFXk2mI7pgKXqwSyc7KWgP70ULzxHeKuIWJutio9krg3//zzet+ZQatORKoThIoJp1gFHaWSn27TXSNPIhuY8KVaDPwd8C5Zf7+74AvED/jOu7K2RrYZ4i9N8+doc4tQpgaX2/DUo99funBa1bgm1VrPDEtQ/b2eh0krO+uqoEQrDuyX/ab6Br1IrpJwssNCcdEJ/Dfgbll/v5ZwKPAuhp7uj5WdzkT/OxyCoFXvE5ErkgYVt6sol+b/pn1W8yqtefgrpnjGMs5iaqHRe+uwB9mzdpk2ctG3ZDE0y0Qti2MgwAXErapc2X+zAJen+BZk3q61lGmzjnrDV2X4mSRiMQuhqAaDGjg/8uRHX2PmUVHhr6mhvkgZyW9jqB7fS09FaMVoYmuUTVPN2kj+7ii2whcRmXFKtqANxP/rG1cTzfAwst1z77Fc8Z5BfdhUZkfX3BVBZ4QCR6Yff+mPrPqCExKC/GKRe9dwIxE1wkPWD/V5LsXsmwPE938cSzy5OJQTCC6M4CLY4ypiyD2Xl0TFl7O6+pRgrEN7wV3tbgkDQ3YV/KD+9pvW7farDpCi6fZ8y9Rce9LlAQHCPQT6E/a2tbuM9E16ok+4icmJfF0zwM6Kn+P6ATOTuDpWiJVDtlzy/wuPPlLhNjdZFTVR4KHvH7/u2JbCSPCtpvmzFSVj4GcneQ6qqoB+uzAgP/DLIeWTXTzST/xj+HEFd0i8EbCcHGlTIj+Ns4q2DzdHPL8QhocXAu8KW7yVJSt/CwD/n2T7uk+ZFatPZuXzJvW0NRwM869P4UEqn4C/7u/3L5ubdbtYtnLJrrDxTOO6E4FLo25iHPAJcBEYFeNPN1BwPb36pSpszsvR7zrRCR2QwOBI6p6f/vYdSvNorVFQXZ9at55zhU+LiIfEpGmRNcLF1A/9Qd1RR6Oe5no5lN0ax1ePpswTBxzfuQsYH6FoiuR6MbxhHoJm0MYdUbPR+ecplK8AeiMfT5INQg0+MEY7ft21kORmRLbZbj9R2bN2OMarvLEG2q96CW/sG5Q1S9PvXvdS3mwk4muebpJRddFXu6EBPd8WnSNX1F+5rUjflu/3sjbNerMQ9rTWrhakKuShZX1pSDg6613bNphVq3u97Vj6ZTmQl/DOG1q6tzbW3gLHleIcBFIc8Jz1QAEGhxw6Ofbm9f8IC92M9E10T2eIpUf32kn3JP1Eo7DSwn3hA9WILpxq9uY6NYhu5bMO7fgedcrtCaQgkFR/vm0Tat/bhY9NSK0iqfv2fvpBdtO9bu+IqKBJ540oDJ2D0wuwkxtlU4P6VBlMoKXhtiqqopwBNUvHDgw8LX22/JTJ91EN3/UOnu5izA8nORFE+B8YDbwdA083WNYeLmu6Fk6pcV5bpEiFybxclX5uV8a+KassO+3PG9Vpolz/xuRU2Z3ewAqAuIQLYAU5bgvS9JslaJ6IAiCLzX6etfcr7x0ME82N9HNH8c3spcY46FS0b2MMDyclDMIz/k+Q3nHOzzzdHM0Eem4dzhxCzVBxESgJ9Dgq1PuWt9tFi3X0xUBaa3gD/5o7Wq0I4u2BzYqwReODPR9dfJnNx3Im83tyFAeF6/xz6B6VBZeHk9YUaqYwn03EIapmyoYu7anmwP2LJk3TQpukSLTEni5PoE+5Pcc+IFZNKszl/ai+u8u8JdsOrDms7NzKLjm6eZXdI/E9HSlQiGbA7wuxQXghYSVrdaW+ftJwssmuvUwWBfi7fHctU7cn5AorBysDPzSN07/5k4repIpnVUV9Jgqz4oGD4tfemhizptSmOjmU3TTaGRfzt9fTBgWTotZkfCWK7qNMe1jDezrhH0dCy4WT65HpDnBkD9IoPdObut+xiyaLcFV1e2gD6o/+K0dm9c/d84o2Is30TXRHU65otsKvImE/TGHMYZwj/g7nDrRKa6nOxR+t7KAIz1Qb5nfttfx4STdZ1Q1UNV/7e89/B25w87kZg0nTAC5Cq+xc+rsBb/dd2vwi8OH+l7o+OLm/Xl9ZhPdfIpu3PZ+EglfOUwnzDhO9R0krE41Fdh8it+Nm0jlk6z9oZHOIJU9qu8Vde8XJwmSp3St+nr/9M9v32tWzRZRElczsAB0PrirAqSnua35N7tu6Xq41Bf89Ix7unfnbqFhX31uPd14c9jLnu6peANhODjtJMZOwuYJ5YzdOJ6utfWrA3YtmTsX5xYhMimBl9sb+Hr/8yvX/NIsmn0BFpGCiJsh4t7vxPtisanwhV1L51219sZUo2kmukbVRDdpePlUv3MZ0FyF+28jLJRRKGPsxnkZA/N0R3iALqboFYvXiri3JKo8RfAj6S99822P2/587gTYuQni3NXOK/7T+MYFf7/nxs7pJrpGPYtutcPLUwgTnqpxVG+oOlV7Gb8X56hSkCASYKTAnraut0SF8BN4MLpFA7130j3dW82ieRZfmeU8dytjCv9v55J55+XhuUx080kv8bNzy/F0zyOsRCVVuv8zo5/XoiES3krvYSi8bJ7uCLBt8emTxHM3KNIVW241GAR9sLd09DGz6KgQ3zGIu04K3j9uX9p1kYmuUY/0Ea/+cjnndIuEoeWxVbz/dsLjSK81PhuJV71oKLxs1BgFKY4d9z4V955kYWV+Pdg/+I2Zd23tNauOGuH1nHNXNXjuf+z95NyzTXSNvIguhOHl15oQJxBmGFdz7BQJjyONq4Lo+lgi1Yiwc8mcc5yTRRLu28dV7n34+vXT717/glk0hYVQyDFUj5bzo6rHVLVXVfs00FK0CKqV8DrEvVsbGm7afmPn5Kza3I4M5Vd04xwyLyd7+RzC/rnleipB9LtS4X2cA8wFVqYsukO1qY0asnnJ9DGFYsOHES5O4OUGEDzsDg08YhZNScigRwP/8z6U2QZRnKAFFWly0KaqE1A5TUTOAGYoMgkYKyJVWZSLSAH0umKjt+5nl3N3FpPoTHTzSZL2fq8VXvaovHfuxsh7Pr3C+zidMFnrZKLbEFN04y5IjASMcS1XKPJBJy5Wne7Qo9JnoXTvxJx1nRlhT/eQwsOn3b5mbdl/c/x/LEO295zeJO0tLQ0DTFZx83DuYtS9GdFzQcal0epvmO63Oo8bzr543m95fN0vTHSNrHu6ryVmbYRNCQoVvJ8/BCYCH6jQ2x1D2EzhAU4cDo7r6Zro1pidn+qYirjrRST2sQ+BI34Q3De5uftJs2jKwuuLUkFi4Ste4uUo9BwjLK26G1ily3h0b2/nGRJ4l6mT96vKn6Qtvop0Cd7CbYtPf2raV3qOZcnetqebX9GN6+k2cPKjOAsIM5fLfXkOA/8J/Crm/bwemHmSf4srur0mujWc0BfieTLmWufxrkR9cgn+g8Heh2S5lXqsd2Q5waTbure2375mxcCx0ic00FtBnwq3B1L6DBFPRN7T0Db24qzZx0Q3nyQJLxdPIroOuAiopIJQD/Ac8PtoFVwpM4ELUhZd83RryN7Z896Ak+tVEzQ0UN0QlIL7Trt7c49ZNFuccU/37knNq78aBPopCH6YpvAiMkMc71y5OJXWoia6RiIGInGJk1lY4MTh42bC0HIlBQ1WAduAbsrrHDScFsIQc9NJPHIT3TrmxRva26BwPci58b3cYEAkeGBy69qfmEWz6/medvvqxyn5/yDws7QynkNv1719WltXpqpV2Z5uPklSX/hknu6cyOssd/IsAU8BhyJx/B1weYwxdyFhUtUG83SzRfukiVeJcDUxM1lVVUX5tfr+Qxs3drgNi2L3Tz71Z/VpE0W8FMq9FDYs6qj4PmfNaglk+apcj8tJd6777e5b5n9OxM1GZE4q3xt0Fpy87gTzg4muUXPRjdvI/kSiK4T7q5X0zj0Yia5GAvxkdE/jK1nMEh4bel2Komt7ujVg56c6pjoKixSZElfHRAhAxqorfqx1SrGq50FFtAByaQoXurJ1SnPFZ0j3HtP1e27svHfSPd2H8jwu9vf5P5rQKA+K01tAUggLyzgHFyg8KhmpMmeim0+G6i+n5ek2EoaWWyq4zmZeGVJ+HthSoehCWPnqjcB/AIMJRdca2NdqYnHN7QjnkChjVTxFzxcn51f/jiWUzAT3KyKCcilCHPH+pd84+G3CyFBu6bqnu3/PknmPqJMPikvu7YoTR+CdtffGzjYysmAx0c2/pxtHdIePi+mEZRnLnZAUeJYwkWqIrcDThEUvKpnYXPTZ7bx8gF+I12EoSTMIo2Jji0sara1WkYUqusyxnlkJRk1+zTF6n2um9QlVnZ3OMSKd1dTY0JyVBYslUpmne6KF2HDRPQfoqOAa/YSh5b5h/++3VB7aFV7dAMERJlLFaXZgomsYI8iMsVv7UZ4ipYiTQvsx6WvLyvOb6ObTdkk93ePDyx5hreVxFVxjP/DMsM8PCI8O7Y1xT+2ElbC8YYuDOIsRa+tnGCMZDFhOEMA6QftTuZ5IE4iJ7iigoc5FN24j++F9aidy6o4/w1lPeExoOGuBF2Le0yVA63Heb1zRNU/XMEYY1WC/IqlUklLU86TQlJVnN9GNubji1N14RnRME79n7PA93XnA/Ar+3icMLZ+oGMZB4DdUHlYaaoDQkVB0kyxGDMNICc/pICmFlwUR3w8yk59kohvfbuNTEt2gSiIQN0t3eHj5ImByhZ/7BK/MNB6iRFgSMk7B+jMIjy2l4ekahjGC+IEUSSmRV0E9z2XmRIKJbjxaCMshSvLxwmCVRLfvJMJXzpgYCp23ETasr+Q83QZO3hkIwqzmF2I8cyNhmLshoeiap2sYI4yImwDaksq1wPe11Geim28mV+j9vZaXe6xKIhC3CMTxx3GmEYZ1K3me3xAeDzoZewizmIMYY/WCyO5xRdeP7G0Yxgihy3AInSCp5MUEMIgfZCaCZaIbjzlU3h/2RJSAA1W6x6SiK4QdhWZU4NH3Ab88xecOEoaY45ypmweclUB0+3nlMSbDMGrMxo0dDS5cQHtpXE/Qg04LR7Py/Ca6leMR1gMek8K1BgkTjuopvAxhg4FGXpkxXNb7RFju8VTP8zSwJsZzj41sX0gguv02hA1j5BgzsWke4i5Oq/CJKDv6Bo5aeDnHtBN2vkkjCWCAsAtPvXm6TdFzVtLgICBsarCljN/dGf1upaJbIEzsaolp/yR9hg3DSIguxPMKXAnMSuV6qqrCpsEjRfN0c8xbCLNo08hcPsDLpQ3rxdMdEt35QFcFzzkA/DoS+3Lu7ZdUXqhCCJsfzCZeaMo8XcMYQfZ3dJ3tnHs/KdU5EChpEKybff8m83SrdK8zYUQbFo8H/jzyAtNgM2FiUb15us1U3rB+C2GCVLne6x+Al2J4u1MJ95rjlIE0T9cwRogNf9cxXp27QZEL0qm5DIoeVoIXsmSHLDU8aAb+hrDE4AOEIcpacxXwTtI7n/ti9DzVYCAS3jjt/doJk8UKZY/9UEQr6Wm5lfBoUaUdZMYQHh2KU4HGEqlqRFDyfSl6L6GalWxxUZgBUowrCKqqKPtEtPJ3WtkSqBfkdTxsW3x6c0Nj018rcr2IpKc7yhYtBWtNdKvHnEj4zge+zqkzZdPkPOAjFXp/p/K6niJ+slM5oh5nwhOgk8q6AQ0QZiRXEi7uBX4BXEdlLQOHEtniiKeFl2vEz7asXffWuQv+Sgay0T3HOdpx3l0IFybSANUHtOR/udK/Kwn9U+98aXcex8K+W+eMU238iApLQcandV1VVeB3/fRvy5I9stjarw24ljCz9nvAdwizYaspvh3AJwkTqNIq/bgzuu9qFWrwiVd9SSJRO62Cv+khPJ9b6bOsBDYRHgOqhE7K2zse7o1bA/sacc0KfFidGQ9k281dkxq95GNDRHdN+sza1TYCQEH2LZ1ztmrDX6uwSMSNT/UDhEMaBD+fedfW3izZJav9dF008d4MvBv4PvDvhCHOtCsOzQeWRh6Zl9545Le8ssl7NTzdoeL+lSwUPMIzyF4Fz/IcJ25wcCo2EWYxn1nhPY4hDC9XugA6Gi1GDMOoVpTjcgpnXdTZsUcK7xSR6xC5JNWQ8h+9XH2hEOivs2afrDex96IJuwv4AGH27I8jQdvIy3uacRgDvB1YDFxJuglcvcBPqW4d4CASmbh2rcSj/hXxinwcA/4TWEi4Z1+pR17pQsdKQBrGMBoTeLIsQ1iFbJk+vaG1oWHy4GDxTOfxJhF3hQjnKtKcVtLUsJe/P1AeGb957WYT3ZET35mE1ZPeTZgV/BRhyPO5SIB3E1aACjhxCcKhKkfjCcOrVwJ/RhhaTnvQPAf8pMoCEDe8XCm7osVO3CSQlYSZz/OrfJ/WS9cwXjXryZiBIm/fd+uCeWVPLCV1AUFxj0iz9MpEZsm0ZqEjUJnrFaUDmDDk2VarDVugwTPS7z8qK7IXuSrkbQgRVlA6K/KAFxKWG9xK2ON1PWFIc2f0/4eSmFp4uc7wecDZhH1kvSrcYz/wSHQf1WTI0622Z7cKSLKHtYFwW6CL6rZKNE/XMF7NTCdyu2r5r54rgOBJ9L46BA/wquHRnvhNDo5IoP/cPn7dmiwavJDjwTRU5KGJMCno9ZH3N+TpDh4nuo2E4WOPcL+4WoNHI8/uYaq/txjUwNP1CTOQk5w1PhJd472kU1rTRNcwynZ0RYCWSme8kWokrqqBKN8d6PcflDvI5BGrwmgaX8Oet2kE7uEw8C3iJR3Vo6e7h3A/N8lnKGEyVQ/hkTATXcMwTiS4quiTpWDgS2fcsz6zx6usDGTtCAjDyiugZiu0uI3sy2U1YXg5KWuBZ2ogiCa6hpFRwYWg26l/15Q71v8my89iolujMUMYVv4SYeJRraim6PqECVRpPM9hql/o5PgjVIZhZElwVTcF6t++4cDa70nG32ET3dqwCfgc8ESNP7eX6lW82h+Jbhp70wFhS8BqhoxKxD9CZRjGCAmuoGsC9f/vrg3r7r/wK1Wbz2pGwb7WqtMDfIYwrFzrFdqxKopuN/BsitdbFV1vGtXJ0xjA6i4bRpYE10eDX5V8/7OrVq77t7c9XtWtMhPdPIyZSHDvAL7KyJQf7KU64eUg8tp7UrzmQcKkrHdQnU5SVnfZMDIzeepBVV2BP/ilKXeufypPz2aiWz3BXQvcDdw3gh5WtTzdw4Sh5TSv7UdCvp/K6j5XIrrm6RpG/Xq2iuqgKr9H/W8U/WPfHn/X1n15e04T3fQpAY8D9xDWgx7JkEi1RHeooEXaPEcYYn476YeYzdM1jPoV3H5UV4M+LEHp4fY7u1+UnCY9Zkl0FdgbiUixTu9vG/Bt4F6gHhorVyORKiAsr7m1Cvc7lJz11iqMTfN0DaP+PNsDIM9roN8P/NKPthxdtyoPyVJ5Ed2jwBcJj6j8GWGpxzidZtIWWgiL/f8UeAB4jHh9bKvBUPKQpmin3kgYq7FHfXzzhEkpX9s8XcMYSYH945yph1DZAPqEHwQ/LQSlJ9vb1m+T5dmsMJVn0SXyHlcDD0Xe0JXABcDU6FlqJcAaeXw7IgF6OBLdequS4pP+MZnNhBWkqhX6eTb6nv9Lyt+nebqGUSOBFQhVVrWEyEFF94jSrYE+g+jvAvVfONjP1q57ukfdQjiLe7p+JLxD4nsmYXP5N0be7xmEtZTTrqGs0WcfAF6MPLLHCPc299exrQ6R3r7yUB/gajZr2EcYvr6EdBtOHIa6DluVUri/QRgd3kIVv4PSy05ZLH+udvZXSorWdEwLqiCqoIKWFEoSRr0GQHpV9ZjAXkV7VHUzyEZhcINqsLEw4O8ev33T4Sx2BkrXhvnAAeMI2/udG/2cBcwmDFO28cpmBnKC59dhXqxGk9h+wq5E3cDTwO8jT2xnnU/iRM98ebQQSUt0V1GdJKrj6YxEN83xuSOKRtSjKDUCV1LcGm0AAAETSURBVAATEl4niL6bF00/K2PlYoqzx3ddEYjXHnel5wNO+UP7bS9WPZ9DQfZ+esEVTmRKTQ0ViPrqqxPnq+igE/r9UtArQXBsUPVw0ZeDhZaBo+MG+wc4vHNAcr4/O5pFdzgNhB1rpkQ/06KfKZEIj+flzkIumqwGCEOxBwgL+W8/7qeHMImrD0b3Ks0wDMMw0S3X65Nh3q47TnSDYV5ugIXqDMMwDMMwDMMwDMMwDMMwDMMwDMMwDMMwDMMwDMMwDMMwDMMwDMMwDMMwDMMwDMMwDMMwDMMwDMMwDMMwDMMwDMMwDMMwDMMwDMMwDMMwDMMwDMMwDMMwDMMwDMMwDMMwDMMwDMMwDMMwDMMwjPzz/wEu4k8eiN1t8wAAAABJRU5ErkJggg=="; + private static final Base64.Decoder DECODER = Base64.getDecoder(); + private final JPanel pan; + + public JLabelLink(String title, int x, int y) { + this.setTitle(title); + this.setSize(x, y); + this.setDefaultCloseOperation(WindowConstants.DISPOSE_ON_CLOSE); + this.setBounds(0, 0, x, y); + this.setLocationRelativeTo(null); + this.setLocationRelativeTo(null); + + pan = new JPanel(); + pan.setBorder(new EmptyBorder(10, 10, 10, 10)); + BoxLayout boxLayout = new BoxLayout(pan, BoxLayout.Y_AXIS); + pan.setLayout(boxLayout); + + this.setContentPane(pan); + this.setVisible(true); + } + + public void addURL(String content, String tooltip) { + JLabel label = new JLabel("" + content + ""); + label.putClientProperty("html.disable", null); + label.setCursor(new Cursor(Cursor.HAND_CURSOR)); + label.setToolTipText(tooltip); + addMouseHandler(label); + pan.add(label); + } + + public void addText(String content) { + JLabel label = new JLabel("" + content + ""); + label.putClientProperty("html.disable", null); + pan.add(label); + } + + public void addLogoImage() { + byte[] imageBytes = DECODER.decode(LOGO_DATA); + try { + BufferedImage img = ImageIO.read(new ByteArrayInputStream(imageBytes)); + ImageIcon icon = new ImageIcon(img); + JLabel label = new JLabel(); + label.setIcon(icon); + pan.add(label); + } catch (IOException e) { + e.printStackTrace(); + } + } + + private void addMouseHandler(final JLabel website) { + website.addMouseListener(new MouseAdapter() { + + @Override + public void mouseClicked(MouseEvent e) { + try { + String href = parseHREF(website.getText()); + Desktop.getDesktop().browse(new URI(Objects.requireNonNull(href))); + } catch (Exception ex) { + Output.outputError("Exception trying to browser from jlabel href - " + ex.getMessage()); + } + } + }); + } + + private static String parseHREF(String html) { + String hrefMarker = "href=\""; + int hrefLoc = html.indexOf(hrefMarker); + if (hrefLoc > 1) { + int hrefEndLoc = html.indexOf("\">"); + if (hrefEndLoc > hrefLoc + 4) { + return html.substring(hrefLoc + hrefMarker.length(), hrefEndLoc); + } + } + return null; + } + +} diff --git a/src/main/java/gui/JWTInterceptTab.java b/src/main/java/gui/JWTInterceptTab.java new file mode 100644 index 0000000..f95b241 --- /dev/null +++ b/src/main/java/gui/JWTInterceptTab.java @@ -0,0 +1,389 @@ +package gui; + +import java.awt.Color; +import java.awt.Dimension; +import java.awt.GridBagConstraints; +import java.awt.GridBagLayout; +import java.awt.GridLayout; +import java.awt.SystemColor; +import java.awt.Toolkit; +import java.awt.datatransfer.StringSelection; +import java.awt.event.ActionListener; +import java.awt.event.KeyListener; +import java.util.Enumeration; + +import javax.swing.AbstractButton; +import javax.swing.ButtonGroup; +import javax.swing.JButton; +import javax.swing.JCheckBox; +import javax.swing.JComboBox; +import javax.swing.JLabel; +import javax.swing.JPanel; +import javax.swing.JPopupMenu; +import javax.swing.JRadioButton; +import javax.swing.JScrollPane; +import javax.swing.JTextArea; +import javax.swing.ScrollPaneConstants; +import javax.swing.SwingConstants; +import javax.swing.SwingUtilities; +import javax.swing.UIManager; +import javax.swing.event.DocumentListener; +import javax.swing.text.JTextComponent; + +import org.fife.ui.rsyntaxtextarea.RSyntaxTextArea; +import org.fife.ui.rsyntaxtextarea.Style; +import org.fife.ui.rsyntaxtextarea.SyntaxConstants; +import org.fife.ui.rsyntaxtextarea.SyntaxScheme; +import org.fife.ui.rsyntaxtextarea.Token; +import org.fife.ui.rsyntaxtextarea.TokenTypes; +import org.fife.ui.rtextarea.RTextScrollPane; + +import app.controllers.ReadableTokenFormat; +import app.helpers.Config; +import app.helpers.Output; +import model.JWTInterceptModel; +import model.Strings; + +public class JWTInterceptTab extends JPanel { + + private static final long serialVersionUID = 1L; + private static final String HTML_DISABLE = "html.disable"; + private final JWTInterceptModel jwtIM; + private final RSyntaxTextAreaFactory rSyntaxTextAreaFactory; + private JRadioButton rdbtnRecalculateSignature; + private JRadioButton rdbtnRandomKey; + private JRadioButton rdbtnOriginalSignature; + private JRadioButton rdbtnChooseSignature; + + private JTextArea jwtKeyArea; + + private RSyntaxTextArea jwtHeaderArea; + private RSyntaxTextArea jwtPayloadArea; + private RSyntaxTextArea jwtSignatureArea; + + private JLabel lblSecretKey; + private JRadioButton rdbtnDontModifySignature; + private JLabel lblProblem; + private JComboBox noneAttackComboBox; + private JLabel lblNewLabel; + private JLabel lblCookieFlags; + private JLabel lbRegisteredClaims; + private JCheckBox chkbxCVEAttack; + private JButton btnCopyPubPrivKeyCVEAttack; + private ButtonGroup btgrp; + + public JWTInterceptTab(JWTInterceptModel jwtIM, RSyntaxTextAreaFactory rSyntaxTextAreaFactory) { + this.jwtIM = jwtIM; + this.rSyntaxTextAreaFactory = rSyntaxTextAreaFactory; + drawGui(); + } + + public void registerActionListeners(ActionListener dontMofiy, ActionListener randomKeyListener, ActionListener originalSignatureListener, ActionListener recalculateSignatureListener, + ActionListener chooseSignatureListener, ActionListener algAttackListener, ActionListener cveAttackListener, DocumentListener syncKey, KeyListener jwtAreaTyped) { + rdbtnDontModifySignature.addActionListener(dontMofiy); + rdbtnRecalculateSignature.addActionListener(randomKeyListener); + rdbtnOriginalSignature.addActionListener(originalSignatureListener); + rdbtnChooseSignature.addActionListener(chooseSignatureListener); + rdbtnRandomKey.addActionListener(recalculateSignatureListener); + noneAttackComboBox.addActionListener(algAttackListener); + chkbxCVEAttack.addActionListener(cveAttackListener); + jwtKeyArea.getDocument().addDocumentListener(syncKey); + jwtHeaderArea.addKeyListener(jwtAreaTyped); + jwtPayloadArea.addKeyListener(jwtAreaTyped); + } + + private void drawGui() { + setLayout(new GridLayout(1, 2, 10, 0)); + + JPanel areasPanel = new JPanel(); + areasPanel.setLayout(new GridLayout(3, 1)); + + JPanel actionPanel = new JPanel(); + actionPanel.setLayout(new GridBagLayout()); + GridBagConstraints c = new GridBagConstraints(); + c.gridx = 0; + c.gridy = 0; + c.anchor = GridBagConstraints.NORTHWEST; + + fixSyntaxArea(); + + jwtHeaderArea = rSyntaxTextAreaFactory.rSyntaxTextArea(3, 20); + jwtHeaderArea.setMarginLinePosition(70); + jwtHeaderArea.setWhitespaceVisible(true); + SyntaxScheme scheme = jwtHeaderArea.getSyntaxScheme(); + Style style = new Style(); + style.foreground = new Color(222, 133, 10); + scheme.setStyle(Token.LITERAL_STRING_DOUBLE_QUOTE, style); + jwtHeaderArea.revalidate(); + jwtHeaderArea.setHighlightCurrentLine(false); + jwtHeaderArea.setCurrentLineHighlightColor(Color.WHITE); + jwtHeaderArea.setSyntaxEditingStyle(SyntaxConstants.SYNTAX_STYLE_JAVASCRIPT); + jwtHeaderArea.setEditable(true); + jwtHeaderArea.setPopupMenu(new JPopupMenu()); + RTextScrollPane headerPane = new RTextScrollPane(jwtHeaderArea); + headerPane.setVerticalScrollBarPolicy(ScrollPaneConstants.VERTICAL_SCROLLBAR_AS_NEEDED); + headerPane.setLineNumbersEnabled(false); + + jwtPayloadArea = rSyntaxTextAreaFactory.rSyntaxTextArea(3, 20); + jwtPayloadArea.setMarginLinePosition(70); + jwtPayloadArea.setWhitespaceVisible(true); + scheme = jwtPayloadArea.getSyntaxScheme(); + style = new Style(); + style.foreground = new Color(222, 133, 10); + scheme.setStyle(TokenTypes.LITERAL_STRING_DOUBLE_QUOTE, style); + jwtPayloadArea.revalidate(); + jwtPayloadArea.setHighlightCurrentLine(false); + jwtPayloadArea.setCurrentLineHighlightColor(Color.WHITE); + jwtPayloadArea.setSyntaxEditingStyle(SyntaxConstants.SYNTAX_STYLE_JAVASCRIPT); + jwtPayloadArea.setEditable(true); + jwtPayloadArea.setPopupMenu(new JPopupMenu()); + RTextScrollPane payloadPane = new RTextScrollPane(jwtPayloadArea); + payloadPane.setVerticalScrollBarPolicy(ScrollPaneConstants.VERTICAL_SCROLLBAR_AS_NEEDED); + payloadPane.setLineNumbersEnabled(false); + + jwtSignatureArea = rSyntaxTextAreaFactory.rSyntaxTextArea(3, 10); + jwtSignatureArea.setMarginLinePosition(70); + jwtSignatureArea.setLineWrap(true); + jwtSignatureArea.setWhitespaceVisible(true); + scheme = jwtSignatureArea.getSyntaxScheme(); + style = new Style(); + style.foreground = new Color(222, 133, 10); + scheme.setStyle(TokenTypes.LITERAL_STRING_DOUBLE_QUOTE, style); + jwtSignatureArea.revalidate(); + jwtSignatureArea.setHighlightCurrentLine(false); + jwtSignatureArea.setCurrentLineHighlightColor(Color.WHITE); + jwtSignatureArea.setSyntaxEditingStyle(SyntaxConstants.SYNTAX_STYLE_NONE); + jwtSignatureArea.setEditable(true); + jwtSignatureArea.setPopupMenu(new JPopupMenu()); + RTextScrollPane signaturePane = new RTextScrollPane(jwtSignatureArea); + signaturePane.setVerticalScrollBarPolicy(ScrollPaneConstants.VERTICAL_SCROLLBAR_AS_NEEDED); + signaturePane.setLineNumbersEnabled(false); + + areasPanel.add(headerPane); + areasPanel.add(payloadPane); + areasPanel.add(signaturePane); + + rdbtnDontModifySignature = new JRadioButton(Strings.DONT_MODIFY); + rdbtnDontModifySignature.setToolTipText(Strings.DONT_MODIFY_TT); + rdbtnDontModifySignature.setSelected(true); + rdbtnDontModifySignature.setHorizontalAlignment(SwingConstants.LEFT); + c.gridy = 1; + actionPanel.add(rdbtnDontModifySignature, c); + + rdbtnRecalculateSignature = new JRadioButton(Strings.RECALC_SIGNATURE); + rdbtnRecalculateSignature.putClientProperty(HTML_DISABLE, null); + rdbtnRecalculateSignature.setToolTipText(Strings.RECALC_SIG_TT); + rdbtnRecalculateSignature.setHorizontalAlignment(SwingConstants.LEFT); + c.gridy = 2; + actionPanel.add(rdbtnRecalculateSignature, c); + + rdbtnOriginalSignature = new JRadioButton(Strings.KEEP_ORIG_SIG); + rdbtnOriginalSignature.setToolTipText(Strings.KEEP_ORIG_SIG_TT); + rdbtnOriginalSignature.setHorizontalAlignment(SwingConstants.LEFT); + c.gridy = 3; + actionPanel.add(rdbtnOriginalSignature, c); + + rdbtnRandomKey = new JRadioButton(Strings.RANDOM_KEY); + rdbtnRandomKey.putClientProperty(HTML_DISABLE, null); + rdbtnRandomKey.setToolTipText(Strings.RANDOM_KEY_TT); + rdbtnRandomKey.setHorizontalAlignment(SwingConstants.LEFT); + c.gridy = 4; + actionPanel.add(rdbtnRandomKey, c); + + rdbtnChooseSignature = new JRadioButton(Strings.CHOOSE_SIG); + rdbtnChooseSignature.setToolTipText(Strings.CHOOSE_SIG_TT); + rdbtnChooseSignature.setHorizontalAlignment(SwingConstants.LEFT); + c.gridy = 5; + actionPanel.add(rdbtnChooseSignature, c); + + lblSecretKey = new JLabel(Strings.RECALC_KEY_INTERCEPT); + c.gridy = 6; + actionPanel.add(lblSecretKey, c); + + jwtKeyArea = new JTextArea(""); + jwtKeyArea.setRows(3); + jwtKeyArea.setLineWrap(false); + jwtKeyArea.setEnabled(false); + jwtKeyArea.setPreferredSize(new Dimension(350, 55)); + + JScrollPane jp = new JScrollPane(jwtKeyArea); + c.gridy = 7; + c.weightx = 0.9; + jp.setMinimumSize(new Dimension(350, 55)); + actionPanel.add(jp, c); + + lblProblem = new JLabel(""); + lblProblem.setForeground(Color.RED); + c.gridy = 8; + actionPanel.add(lblProblem, c); + + lblNewLabel = new JLabel("Alg None Attack:"); + c.gridy = 9; + actionPanel.add(lblNewLabel, c); + + noneAttackComboBox = new JComboBox<>(); + noneAttackComboBox.setMaximumSize(new Dimension(300, 20)); + noneAttackComboBox.setPreferredSize(new Dimension(300, 20)); + c.gridy = 10; + actionPanel.add(noneAttackComboBox, c); + + chkbxCVEAttack = new JCheckBox("CVE-2018-0114 Attack"); + chkbxCVEAttack.setToolTipText("The public and private key used can be found in src/app/helpers/Strings.java"); + chkbxCVEAttack.setHorizontalAlignment(SwingConstants.LEFT); + c.gridy = 11; + actionPanel.add(chkbxCVEAttack, c); + + lblCookieFlags = new JLabel(""); + lblCookieFlags.putClientProperty(HTML_DISABLE, null); + c.gridy = 12; + actionPanel.add(lblCookieFlags, c); + + lbRegisteredClaims = new JLabel(); + lbRegisteredClaims.putClientProperty(HTML_DISABLE, null); + lbRegisteredClaims.setBackground(SystemColor.controlHighlight); + c.gridy = 13; + actionPanel.add(lbRegisteredClaims, c); + + btnCopyPubPrivKeyCVEAttack = new JButton("Copy used public & private\r\nkey to clipboard used in CVE attack"); + btnCopyPubPrivKeyCVEAttack.setVisible(false); + c.gridy = 14; + actionPanel.add(btnCopyPubPrivKeyCVEAttack, c); + + add(areasPanel); + add(actionPanel); + + setVisible(true); + + btgrp = new ButtonGroup(); + btgrp.add(rdbtnDontModifySignature); + btgrp.add(rdbtnOriginalSignature); + btgrp.add(rdbtnRandomKey); + btgrp.add(rdbtnRecalculateSignature); + btgrp.add(rdbtnChooseSignature); + + btnCopyPubPrivKeyCVEAttack.addActionListener(a -> Toolkit.getDefaultToolkit().getSystemClipboard() + .setContents(new StringSelection("Public Key:\r\n" + Config.cveAttackModePublicKey + "\r\n\r\nPrivate Key:\r\n" + Config.cveAttackModePrivateKey), null)); + + noneAttackComboBox.addItem(" -"); + noneAttackComboBox.addItem("Alg: none"); + noneAttackComboBox.addItem("Alg: None"); + noneAttackComboBox.addItem("Alg: nOnE"); + noneAttackComboBox.addItem("Alg: NONE"); + + } + + private void fixSyntaxArea() { + JTextComponent.removeKeymap("RTextAreaKeymap"); + UIManager.put("RSyntaxTextAreaUI.actionMap", null); + UIManager.put("RSyntaxTextAreaUI.inputMap", null); + UIManager.put("RTextAreaUI.actionMap", null); + UIManager.put("RTextAreaUI.inputMap", null); + } + + public void setProblemLbl(String txt) { + lblProblem.setText(txt); + } + + public void setRadiosState(boolean enabled) { + Enumeration buttons = btgrp.getElements(); + while (buttons.hasMoreElements()) { + buttons.nextElement().setEnabled(enabled); + } + } + + public AbstractButton getRdbtnDontModify() { + return rdbtnDontModifySignature; + } + + public JRadioButton getRdbtnChooseSignature() { + return rdbtnChooseSignature; + } + + public JRadioButton getRdbtnRecalculateSignature() { + return rdbtnRecalculateSignature; + } + + public JComboBox getNoneAttackComboBox() { + return noneAttackComboBox; + } + + public JCheckBox getCVEAttackCheckBox() { + return chkbxCVEAttack; + } + + public JRadioButton getRdbtnRandomKey() { + return rdbtnRandomKey; + } + + public JButton getCVECopyBtn() { + return btnCopyPubPrivKeyCVEAttack; + } + + public JRadioButton getRdbtnOriginalSignature() { + return rdbtnOriginalSignature; + } + + public void updateSetView(final boolean reset) { + updateSetView(reset, false); + } + + public void updateSetView(final boolean reset, final boolean noKeyUpdate) { + SwingUtilities.invokeLater(() -> { + Output.output("Updating view - reset: " + reset); + + if (reset) { + rdbtnDontModifySignature.setSelected(true); + jwtKeyArea.setText(""); + jwtKeyArea.setEnabled(false); + } else { + jwtHeaderArea.setText(ReadableTokenFormat.jsonBeautify(jwtIM.getJwToken().getHeaderJson())); + jwtPayloadArea.setText(ReadableTokenFormat.jsonBeautify(jwtIM.getJwToken().getPayloadJson())); + jwtSignatureArea.setText(jwtIM.getJwToken().getSignature()); + if (noKeyUpdate) { + jwtKeyArea.setText(jwtIM.getJWTKey()); + } + } + lblProblem.setText(jwtIM.getProblemDetail()); + // -> response of https://oz-web.com/jwt/request_cookie.php + if (jwtIM.getcFW().isCookie()) { + lblCookieFlags.setText(jwtIM.getcFW().toHTMLString()); + } else { + lblCookieFlags.setText(""); + } + lbRegisteredClaims.setText(jwtIM.getTimeClaimsAsText()); + }); + } + + public RSyntaxTextArea getJwtHeaderArea() { + return jwtHeaderArea; + } + + public RSyntaxTextArea getJwtPayloadArea() { + return jwtPayloadArea; + } + + public RSyntaxTextArea getJwtSignatureArea() { + return jwtSignatureArea; + } + + public void setKeyFieldState(boolean state) { + jwtKeyArea.setEnabled(state); + } + + public String getSelectedData() { + return jwtPayloadArea.getSelectedText(); + } + + public String getKeyFieldValue() { + return jwtKeyArea.getText(); + } + + public JTextArea getKeyField() { + return jwtKeyArea; + } + + public void setKeyFieldValue(String string) { + jwtKeyArea.setText(string); + } + +} diff --git a/src/main/java/gui/JWTSuiteTab.java b/src/main/java/gui/JWTSuiteTab.java new file mode 100644 index 0000000..c0776bf --- /dev/null +++ b/src/main/java/gui/JWTSuiteTab.java @@ -0,0 +1,266 @@ +package gui; + +import java.awt.Color; +import java.awt.Desktop; +import java.awt.Dimension; +import java.awt.Font; +import java.awt.GridBagConstraints; +import java.awt.GridBagLayout; +import java.awt.Insets; +import java.io.File; +import java.io.IOException; +import java.io.Serial; + +import javax.swing.JButton; +import javax.swing.JLabel; +import javax.swing.JPanel; +import javax.swing.JPopupMenu; +import javax.swing.JTextArea; +import javax.swing.SwingUtilities; +import javax.swing.UIManager; +import javax.swing.event.DocumentListener; +import javax.swing.text.JTextComponent; + +import org.fife.ui.rsyntaxtextarea.RSyntaxTextArea; +import org.fife.ui.rsyntaxtextarea.SyntaxConstants; +import org.fife.ui.rsyntaxtextarea.SyntaxScheme; +import org.fife.ui.rsyntaxtextarea.Token; +import org.fife.ui.rsyntaxtextarea.Style; +import org.fife.ui.rtextarea.RTextScrollPane; + +import app.helpers.Config; +import model.JWTSuiteTabModel; +import model.Strings; + +public class JWTSuiteTab extends JPanel { + + @Serial + private static final long serialVersionUID = 1L; + + public static final String TAHOMA = "Tahoma"; + private JTextArea jwtInputField; + private RSyntaxTextArea jwtOuputField; + private JButton jwtSignatureButton; + private JTextArea jwtKeyArea; + private final JWTSuiteTabModel jwtSTM; + private final RSyntaxTextAreaFactory rSyntaxTextAreaFactory; + private JLabel lbRegisteredClaims; + private JLabel lblExtendedVerificationInfo; + + public JWTSuiteTab(JWTSuiteTabModel jwtSTM, RSyntaxTextAreaFactory rSyntaxTextAreaFactory) { + this.rSyntaxTextAreaFactory = rSyntaxTextAreaFactory; + drawGui(); + this.jwtSTM = jwtSTM; + } + + public void updateSetView() { + SwingUtilities.invokeLater(() -> { + if (!jwtInputField.getText().equals(jwtSTM.getJwtInput())) { + jwtInputField.setText(jwtSTM.getJwtInput()); + } + if (!jwtSignatureButton.getText().equals(jwtSTM.getVerificationLabel())) { + jwtSignatureButton.setText(jwtSTM.getVerificationLabel()); + } + if (!jwtOuputField.getText().equals(jwtSTM.getJwtJSON())) { + jwtOuputField.setText(jwtSTM.getJwtJSON()); + } + if (!jwtKeyArea.getText().equals(jwtSTM.getJwtKey())) { + jwtKeyArea.setText(jwtSTM.getJwtKey()); + } + if (!jwtSignatureButton.getBackground().equals(jwtSTM.getJwtSignatureColor())) { + jwtSignatureButton.setBackground(jwtSTM.getJwtSignatureColor()); + } + if (jwtKeyArea.getText().equals("")) { + jwtSTM.setJwtSignatureColor(new JButton().getBackground()); + jwtSignatureButton.setBackground(jwtSTM.getJwtSignatureColor()); + } + lblExtendedVerificationInfo.setText(jwtSTM.getVerificationResult()); + lbRegisteredClaims.setText(jwtSTM.getTimeClaimsAsText()); + jwtOuputField.setCaretPosition(0); + }); + } + + public void registerDocumentListener(DocumentListener jwtInputListener, DocumentListener jwtKeyListener) { + jwtInputField.getDocument().addDocumentListener(jwtInputListener); + jwtKeyArea.getDocument().addDocumentListener(jwtKeyListener); + } + + private void drawGui() { + GridBagLayout gridBagLayout = new GridBagLayout(); + gridBagLayout.columnWidths = new int[] { 10, 0, 0, 0 }; + gridBagLayout.rowHeights = new int[] { 10, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 }; + gridBagLayout.columnWeights = new double[] { 0.0, 1.0, 0.0, Double.MIN_VALUE }; + gridBagLayout.rowWeights = new double[] { 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, Double.MIN_VALUE }; + setLayout(gridBagLayout); + + JLabel lblPasteJwtToken = new JLabel(Strings.ENTER_JWT); + lblPasteJwtToken.setFont(new Font(TAHOMA, Font.BOLD, 12)); + GridBagConstraints gbc_lblPasteJwtToken = new GridBagConstraints(); + gbc_lblPasteJwtToken.anchor = GridBagConstraints.SOUTHWEST; + gbc_lblPasteJwtToken.insets = new Insets(0, 0, 5, 5); + gbc_lblPasteJwtToken.gridx = 1; + gbc_lblPasteJwtToken.gridy = 1; + add(lblPasteJwtToken, gbc_lblPasteJwtToken); + + JButton creditButton = new JButton("About"); + creditButton.addActionListener(arg0 -> { + JLabelLink jLabelLink = new JLabelLink(Strings.CREDIT_TITLE, 530, 565); + + jLabelLink.addText("

About JWT4B

JSON Web Tokens (also known as JWT4B) is developed by Oussama Zgheb
"); + jLabelLink.addURL("Mantainer Website", "zgheb.com"); + jLabelLink.addURL("GitHub Repository", "github.com/ozzi/JWT4B"); + jLabelLink.addText("
"); + jLabelLink.addText("JWT4B, excluding the libraries mentioned below and the Burp extender classes, uses the GPL 3 license."); + jLabelLink.addURL("· RSyntaxTextArea", + "github.com/bobbylight/RSyntaxTextArea"); + jLabelLink.addURL("· Auth0 -java-jwt", "github.com/auth0/java-jwt"); + jLabelLink.addURL("· Apache Commons Lang", "apache.org"); + jLabelLink.addText("
"); + jLabelLink.addText("Thanks to:
· Compass Security AG for providing development time for the initial version
"); + jLabelLink.addURL("  compass-security.com
", "compass-security.com"); + jLabelLink.addText("· Brainloop for providing broader token support"); + jLabelLink.addURL("  brainloop.com
", "brainloop.com"); + jLabelLink.addText("· Cyrill for the help porting JWT4B to the Montaya API"); + jLabelLink.addURL("  github.com/bcyrill

", "github.com/bcyrill"); + jLabelLink.addLogoImage(); + }); + GridBagConstraints gbc_creditButton = new GridBagConstraints(); + gbc_creditButton.insets = new Insets(0, 0, 5, 0); + gbc_creditButton.gridx = 2; + gbc_creditButton.gridy = 1; + gbc_creditButton.fill = GridBagConstraints.HORIZONTAL; + add(creditButton, gbc_creditButton); + + JButton configButton = new JButton("Change Config"); + configButton.addActionListener(arg0 -> { + File file = new File(Config.configPath); + Desktop desktop = Desktop.getDesktop(); + try { + desktop.open(file); + } catch (IOException e) { + System.err.println("Error using Desktop API - " + e.getMessage() + " - " + e.getCause()); + } + }); + + GridBagConstraints gbc_configButton = new GridBagConstraints(); + gbc_configButton.insets = new Insets(0, 0, 5, 0); + gbc_configButton.gridx = 2; + gbc_configButton.gridy = 2; + gbc_configButton.fill = GridBagConstraints.HORIZONTAL; + add(configButton, gbc_configButton); + + jwtInputField = new JTextArea(); + jwtInputField.setBorder(UIManager.getLookAndFeel().getDefaults().getBorder("TextField.border")); + jwtInputField.setRows(2); + jwtInputField.setLineWrap(true); + jwtInputField.setWrapStyleWord(true); + + GridBagConstraints gbc_jwtInputField = new GridBagConstraints(); + gbc_jwtInputField.insets = new Insets(0, 0, 5, 5); + gbc_jwtInputField.fill = GridBagConstraints.BOTH; + gbc_jwtInputField.gridx = 1; + gbc_jwtInputField.gridy = 2; + add(jwtInputField, gbc_jwtInputField); + + JLabel lblEnterSecret = new JLabel(Strings.ENTER_SECRET_KEY); + lblEnterSecret.setFont(new Font(TAHOMA, Font.BOLD, 12)); + GridBagConstraints gbc_lblEnterSecret = new GridBagConstraints(); + gbc_lblEnterSecret.anchor = GridBagConstraints.WEST; + gbc_lblEnterSecret.insets = new Insets(0, 0, 5, 5); + gbc_lblEnterSecret.gridx = 1; + gbc_lblEnterSecret.gridy = 3; + add(lblEnterSecret, gbc_lblEnterSecret); + + jwtKeyArea = new JTextArea(); + jwtKeyArea.setBorder(UIManager.getLookAndFeel().getDefaults().getBorder("TextField.border")); + GridBagConstraints gbc_jwtKeyField = new GridBagConstraints(); + gbc_jwtKeyField.insets = new Insets(0, 0, 5, 5); + gbc_jwtKeyField.fill = GridBagConstraints.HORIZONTAL; + gbc_jwtKeyField.gridx = 1; + gbc_jwtKeyField.gridy = 4; + add(jwtKeyArea, gbc_jwtKeyField); + jwtKeyArea.setColumns(10); + + jwtSignatureButton = new JButton(""); + Dimension preferredSize = new Dimension(400, 30); + jwtSignatureButton.setPreferredSize(preferredSize); + + GridBagConstraints gbc_jwtSignatureButton = new GridBagConstraints(); + gbc_jwtSignatureButton.insets = new Insets(0, 0, 5, 5); + gbc_jwtSignatureButton.gridx = 1; + gbc_jwtSignatureButton.gridy = 6; + add(jwtSignatureButton, gbc_jwtSignatureButton); + + GridBagConstraints gbc_jwtOuputField = new GridBagConstraints(); + gbc_jwtOuputField.insets = new Insets(0, 0, 5, 5); + gbc_jwtOuputField.fill = GridBagConstraints.BOTH; + gbc_jwtOuputField.gridx = 1; + gbc_jwtOuputField.gridy = 9; + + JTextComponent.removeKeymap("RTextAreaKeymap"); + jwtOuputField = rSyntaxTextAreaFactory.rSyntaxTextArea(); + UIManager.put("RSyntaxTextAreaUI.actionMap", null); + UIManager.put("RSyntaxTextAreaUI.inputMap", null); + UIManager.put("RTextAreaUI.actionMap", null); + UIManager.put("RTextAreaUI.inputMap", null); + jwtOuputField.setWhitespaceVisible(true); + + SyntaxScheme scheme = jwtOuputField.getSyntaxScheme(); + Style style = new Style(); + style.foreground = new Color(222, 133, 10); + scheme.setStyle(Token.LITERAL_STRING_DOUBLE_QUOTE, style); + jwtOuputField.revalidate(); + jwtOuputField.setHighlightCurrentLine(false); + jwtOuputField.setCurrentLineHighlightColor(Color.WHITE); + jwtOuputField.setSyntaxEditingStyle(SyntaxConstants.SYNTAX_STYLE_JAVASCRIPT); + jwtOuputField.setEditable(false); + // no context menu on right-click + jwtOuputField.setPopupMenu(new JPopupMenu()); + + // hopefully fixing: + // java.lang.ClassCastException: class + // javax.swing.plaf.nimbus.DerivedColor$UIResource cannot be cast to class + // java.lang.Boolean (javax.swing.plaf.nimbus.DerivedColor$UIResource is in + // module java.desktop of loader 'bootstrap'; + // java.lang.Boolean is in module java.base of loader 'bootstrap') + SwingUtilities.invokeLater(() -> { + RTextScrollPane sp = new RTextScrollPane(jwtOuputField); + sp.setLineNumbersEnabled(false); + add(sp, gbc_jwtOuputField); + }); + + lblExtendedVerificationInfo = new JLabel(""); + GridBagConstraints gbc_lblExtendedVerificationInfo = new GridBagConstraints(); + gbc_lblExtendedVerificationInfo.insets = new Insets(0, 0, 5, 5); + gbc_lblExtendedVerificationInfo.gridx = 1; + gbc_lblExtendedVerificationInfo.gridy = 7; + add(lblExtendedVerificationInfo, gbc_lblExtendedVerificationInfo); + + JLabel lblDecodedJwt = new JLabel(Strings.DECODED_JWT); + lblDecodedJwt.setFont(new Font(TAHOMA, Font.BOLD, 12)); + GridBagConstraints gbc_lblDecodedJwt = new GridBagConstraints(); + gbc_lblDecodedJwt.anchor = GridBagConstraints.WEST; + gbc_lblDecodedJwt.insets = new Insets(0, 0, 5, 5); + gbc_lblDecodedJwt.gridx = 1; + gbc_lblDecodedJwt.gridy = 8; + add(lblDecodedJwt, gbc_lblDecodedJwt); + + lbRegisteredClaims = new JLabel(); + lbRegisteredClaims.putClientProperty("html.disable", false); + lbRegisteredClaims.setBackground(new Color(238, 238, 238)); + GridBagConstraints gbc_lbRegisteredClaims = new GridBagConstraints(); + gbc_lbRegisteredClaims.fill = GridBagConstraints.BOTH; + gbc_lbRegisteredClaims.insets = new Insets(0, 0, 5, 5); + gbc_lbRegisteredClaims.gridx = 1; + gbc_lbRegisteredClaims.gridy = 11; + add(lbRegisteredClaims, gbc_lbRegisteredClaims); + } + + public String getJWTInput() { + return jwtInputField.getText(); + } + + public String getKeyInput() { + return jwtKeyArea.getText(); + } +} diff --git a/src/main/java/gui/JWTViewTab.java b/src/main/java/gui/JWTViewTab.java new file mode 100644 index 0000000..65b4209 --- /dev/null +++ b/src/main/java/gui/JWTViewTab.java @@ -0,0 +1,236 @@ +package gui; + +import java.awt.Color; +import java.awt.Dimension; +import java.awt.Font; +import java.awt.GridBagConstraints; +import java.awt.GridBagLayout; +import java.awt.Insets; +import java.awt.SystemColor; +import java.awt.event.ActionEvent; +import java.awt.event.ActionListener; + +import javax.swing.JButton; +import javax.swing.JLabel; +import javax.swing.JPanel; +import javax.swing.JPopupMenu; +import javax.swing.JTextArea; +import javax.swing.SwingUtilities; +import javax.swing.UIManager; +import javax.swing.event.DocumentListener; +import javax.swing.text.JTextComponent; + +import org.fife.ui.rsyntaxtextarea.RSyntaxTextArea; +import org.fife.ui.rsyntaxtextarea.Style; +import org.fife.ui.rsyntaxtextarea.SyntaxConstants; +import org.fife.ui.rsyntaxtextarea.SyntaxScheme; +import org.fife.ui.rsyntaxtextarea.Token; +import org.fife.ui.rtextarea.RTextScrollPane; + +import app.algorithm.AlgorithmType; +import model.JWTTabModel; +import model.Strings; + +public class JWTViewTab extends JPanel { + + private static final long serialVersionUID = 1L; + private RSyntaxTextArea outputField; + private JTextArea jwtKeyArea; + private JLabel keyLabel; + private JButton verificationIndicator; + private final JWTTabModel jwtTM; + private final RSyntaxTextAreaFactory rSyntaxTextAreaFactory; + private JLabel lblCookieFlags; + private JLabel lbRegisteredClaims; + + public JWTViewTab(JWTTabModel jwtTM, RSyntaxTextAreaFactory rSyntaxTextAreaFactory) { + this.rSyntaxTextAreaFactory = rSyntaxTextAreaFactory; + drawPanel(); + this.jwtTM = jwtTM; + } + + public void registerDocumentListener(DocumentListener inputFieldListener) { + jwtKeyArea.getDocument().addDocumentListener(inputFieldListener); + } + + private void drawPanel() { + GridBagLayout gridBagLayout = new GridBagLayout(); + gridBagLayout.columnWidths = new int[] { 10, 447, 0, 0 }; + gridBagLayout.rowHeights = new int[] { 10, 10, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 }; + gridBagLayout.columnWeights = new double[] { 0.0, 1.0, 0.0, Double.MIN_VALUE }; + gridBagLayout.rowWeights = new double[] { 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, Double.MIN_VALUE }; + setLayout(gridBagLayout); + + keyLabel = new JLabel(" "); + keyLabel.setFont(new Font("Tahoma", Font.BOLD, 12)); + GridBagConstraints gbc_inputLabel1 = new GridBagConstraints(); + gbc_inputLabel1.fill = GridBagConstraints.VERTICAL; + gbc_inputLabel1.insets = new Insets(0, 0, 5, 5); + gbc_inputLabel1.anchor = GridBagConstraints.WEST; + gbc_inputLabel1.gridx = 1; + gbc_inputLabel1.gridy = 1; + add(keyLabel, gbc_inputLabel1); + + jwtKeyArea = new JTextArea(); + jwtKeyArea.setBorder(UIManager.getLookAndFeel().getDefaults().getBorder("TextField.border")); + GridBagConstraints gbc_inputField1 = new GridBagConstraints(); + gbc_inputField1.insets = new Insets(0, 0, 5, 5); + gbc_inputField1.fill = GridBagConstraints.HORIZONTAL; + gbc_inputField1.gridx = 1; + gbc_inputField1.gridy = 2; + add(jwtKeyArea, gbc_inputField1); + jwtKeyArea.setColumns(10); + + verificationIndicator = new JButton(""); + verificationIndicator.setText(Strings.NO_SECRET_PROVIDED); + verificationIndicator.addActionListener(new ActionListener() { + + public void actionPerformed(ActionEvent e) { + } + }); + Dimension preferredSize = new Dimension(400, 30); + verificationIndicator.setPreferredSize(preferredSize); + GridBagConstraints gbc_validIndicator = new GridBagConstraints(); + gbc_validIndicator.insets = new Insets(0, 0, 5, 5); + gbc_validIndicator.gridx = 1; + gbc_validIndicator.gridy = 4; + add(verificationIndicator, gbc_validIndicator); + + JTextComponent.removeKeymap("RTextAreaKeymap"); + outputField = rSyntaxTextAreaFactory.rSyntaxTextArea(); + UIManager.put("RSyntaxTextAreaUI.actionMap", null); + UIManager.put("RSyntaxTextAreaUI.inputMap", null); + UIManager.put("RTextAreaUI.actionMap", null); + UIManager.put("RTextAreaUI.inputMap", null); + + outputField.setWhitespaceVisible(true); + SyntaxScheme scheme = outputField.getSyntaxScheme(); + Style style = new Style(); + style.foreground = new Color(222, 133, 10); + scheme.setStyle(Token.LITERAL_STRING_DOUBLE_QUOTE, style); + outputField.revalidate(); + outputField.setHighlightCurrentLine(false); + outputField.setCurrentLineHighlightColor(Color.WHITE); + outputField.setSyntaxEditingStyle(SyntaxConstants.SYNTAX_STYLE_JAVASCRIPT); + outputField.setEditable(false); + outputField.setPopupMenu(new JPopupMenu()); // no context menu on right-click + + JLabel outputLabel = new JLabel("JWT"); + outputLabel.setFont(new Font("Tahoma", Font.BOLD, 12)); + GridBagConstraints gbc_outputLabel = new GridBagConstraints(); + gbc_outputLabel.anchor = GridBagConstraints.WEST; + gbc_outputLabel.insets = new Insets(0, 0, 5, 5); + gbc_outputLabel.gridx = 1; + gbc_outputLabel.gridy = 5; + add(outputLabel, gbc_outputLabel); + + lbRegisteredClaims = new JLabel(); + lbRegisteredClaims.putClientProperty("html.disable", null); + lbRegisteredClaims.setBackground(SystemColor.controlHighlight); + GridBagConstraints gbc_lbRegisteredClaims = new GridBagConstraints(); + gbc_lbRegisteredClaims.fill = GridBagConstraints.BOTH; + gbc_lbRegisteredClaims.insets = new Insets(0, 0, 5, 5); + gbc_lbRegisteredClaims.gridx = 1; + gbc_lbRegisteredClaims.gridy = 8; + add(lbRegisteredClaims, gbc_lbRegisteredClaims); + + lblCookieFlags = new JLabel(" "); + lblCookieFlags.putClientProperty("html.disable", null); + lblCookieFlags.setFont(new Font("Tahoma", Font.BOLD, 12)); + GridBagConstraints gbc_lblCookieFlags = new GridBagConstraints(); + gbc_lblCookieFlags.anchor = GridBagConstraints.SOUTHWEST; + gbc_lblCookieFlags.insets = new Insets(0, 0, 5, 5); + gbc_lblCookieFlags.gridx = 1; + gbc_lblCookieFlags.gridy = 9; + add(lblCookieFlags, gbc_lblCookieFlags); + + RTextScrollPane sp = new RTextScrollPane(outputField); + sp.setLineNumbersEnabled(false); + + GridBagConstraints gbc_outputfield = new GridBagConstraints(); + gbc_outputfield.insets = new Insets(0, 0, 5, 5); + gbc_outputfield.fill = GridBagConstraints.BOTH; + gbc_outputfield.gridx = 1; + gbc_outputfield.gridy = 6; + add(sp, gbc_outputfield); + + } + + public JTextArea getOutputfield() { + return outputField; + } + + public String getKeyValue() { + return jwtKeyArea.getText(); + } + + public void setKeyValue(String value) { + jwtKeyArea.setText(value); + } + + public void setVerificationResult(String value) { + verificationIndicator.setText(value); + } + + public void setVerificationResultColor(Color verificationResultColor) { + verificationIndicator.setBackground(verificationResultColor); + } + + public void setCaret() { + outputField.setCaretPosition(0); + } + + public String getSelectedData() { + return getOutputfield().getSelectedText(); + } + + public void updateSetView(AlgorithmType algorithmType) { + SwingUtilities.invokeLater(new Runnable() { + + public void run() { + if (!jwtTM.getJWTJSON().equals(outputField.getText())) { + outputField.setText(jwtTM.getJWTJSON()); + } + if (!jwtTM.getKeyLabel().equals(keyLabel.getText())) { + keyLabel.setText(jwtTM.getKeyLabel()); + } + if (!jwtTM.getKey().equals(jwtKeyArea.getText())) { + jwtKeyArea.setText(jwtTM.getKey()); + } + if (!jwtTM.getVerificationColor().equals(verificationIndicator.getBackground())) { + verificationIndicator.setBackground(jwtTM.getVerificationColor()); + } + if (!jwtTM.getVerificationLabel().equals(verificationIndicator.getText())) { + if (jwtTM.getVerificationLabel().equals("")) { + verificationIndicator.setText(Strings.NO_SECRET_PROVIDED); + } else { + verificationIndicator.setText(jwtTM.getVerificationLabel()); + } + + } + if (algorithmType.equals(AlgorithmType.SYMMETRIC)) { + keyLabel.setText("Secret"); + jwtKeyArea.setEnabled(true); + } + if (algorithmType.equals(AlgorithmType.ASYMMETRIC)) { + keyLabel.setText("Public Key"); + jwtKeyArea.setEnabled(true); + } + if (algorithmType.equals(AlgorithmType.NONE)) { + keyLabel.setText(""); + jwtKeyArea.setEnabled(false); + jwtKeyArea.setEnabled(false); + } + + if (jwtTM.getcFW().isCookie()) { + lblCookieFlags.setText(jwtTM.getcFW().toHTMLString()); + } else { + lblCookieFlags.setText(""); + } + setCaret(); + lbRegisteredClaims.setText(jwtTM.getTimeClaimsAsText()); + } + }); + } + +} diff --git a/src/main/java/gui/RSyntaxTextAreaFactory.java b/src/main/java/gui/RSyntaxTextAreaFactory.java new file mode 100644 index 0000000..9e4cfa3 --- /dev/null +++ b/src/main/java/gui/RSyntaxTextAreaFactory.java @@ -0,0 +1,68 @@ +package gui; + +import static app.helpers.Output.outputError; + +import java.io.IOException; + +import burp.api.montoya.ui.UserInterface; +import org.fife.ui.rsyntaxtextarea.RSyntaxTextArea; +import org.fife.ui.rsyntaxtextarea.Theme; + +public class RSyntaxTextAreaFactory { + + private final ThemeDetector themeDetector; + + public RSyntaxTextAreaFactory(UserInterface userInterface) { + this.themeDetector = new ThemeDetector(userInterface); + } + + RSyntaxTextArea rSyntaxTextArea() { + return new BurpThemeAwareRSyntaxTextArea(themeDetector); + } + + RSyntaxTextArea rSyntaxTextArea(int rows, int cols) { + return new BurpThemeAwareRSyntaxTextArea(themeDetector, rows, cols); + } + + private static class BurpThemeAwareRSyntaxTextArea extends RSyntaxTextArea { + + private static final long serialVersionUID = 1L; + private static final String THEME_PATH = "/org/fife/ui/rsyntaxtextarea/themes/"; //NOSONAR + private static final String DARK_THEME = THEME_PATH + "dark.xml"; + private static final String LIGHT_THEME = THEME_PATH + "default.xml"; + + private final ThemeDetector themeDetector; + + private BurpThemeAwareRSyntaxTextArea(ThemeDetector themeDetector) { + this.themeDetector = themeDetector; + applyTheme(); + } + + public BurpThemeAwareRSyntaxTextArea(ThemeDetector themeDetector, int rows, int cols) { + super(rows, cols); + this.themeDetector = themeDetector; + applyTheme(); + } + + @Override + public void updateUI() { + super.updateUI(); + + if (themeDetector != null) { + applyTheme(); + } + } + + private void applyTheme() { + String themeResource = themeDetector.isLightTheme() ? LIGHT_THEME : DARK_THEME; + + try { + Theme theme = Theme.load(getClass().getResourceAsStream(themeResource)); + theme.apply(this); + } catch (IOException e) { + outputError("Unable to apply rsyntax theme: " + e.getMessage()); + } + } + } + +} diff --git a/src/main/java/gui/ThemeDetector.java b/src/main/java/gui/ThemeDetector.java new file mode 100644 index 0000000..2411ae3 --- /dev/null +++ b/src/main/java/gui/ThemeDetector.java @@ -0,0 +1,25 @@ +package gui; + +import static java.awt.Color.WHITE; + +import javax.swing.JLabel; + +import burp.api.montoya.ui.UserInterface; +import model.Settings; + +public class ThemeDetector { + + private final UserInterface userInterface; + + ThemeDetector(UserInterface userInterface) { + this.userInterface = userInterface; + } + + boolean isLightTheme() { + JLabel label = new JLabel(); + userInterface.applyThemeToComponent(label); + boolean isLight = label.getBackground().equals(WHITE); + Settings.isLight = isLight; // TODO not so nice, since settings are static + return isLight; + } +} diff --git a/src/model/CustomJWToken.java b/src/main/java/model/CustomJWToken.java similarity index 98% rename from src/model/CustomJWToken.java rename to src/main/java/model/CustomJWToken.java index 1c76284..7f72fef 100644 --- a/src/model/CustomJWToken.java +++ b/src/main/java/model/CustomJWToken.java @@ -57,7 +57,7 @@ public CustomJWToken(String token) { for (int result = gis.read(); result != -1; result = gis.read()) { buf.write((byte) result); } - payloadJson = buf.toString("UTF-8"); + payloadJson = buf.toString(StandardCharsets.UTF_8); } } catch (IOException e) { Output.outputError("Could not gunzip JSON - " + e.getMessage()); @@ -108,7 +108,7 @@ private void checkRegisteredClaims(String payloadJson) { java.util.Date time = new java.util.Date(expUT * 1000); String expDate = time.toString(); boolean expValid = expUT > curUT; - timeClaimList.add(new TimeClaim("[exp] Expired", expDate, expUT, expValid)); + timeClaimList.add(new TimeClaim("[exp] Expired", expDate, expUT, expValid,true)); } catch (Exception e) { Output.output("Could not parse claim (exp) - " + e.getMessage() + " - " + e.getCause()); } @@ -121,7 +121,7 @@ private void checkRegisteredClaims(String payloadJson) { java.util.Date time = new java.util.Date(nbfUT * 1000); String nbfDate = time.toString(); boolean nbfValid = nbfUT <= curUT; - timeClaimList.add(new TimeClaim("[nbf] Not before", nbfDate, nbfUT, nbfValid)); + timeClaimList.add(new TimeClaim("[nbf] Not before", nbfDate, nbfUT, nbfValid,true)); } catch (Exception e) { Output.output("Could not parse claim (nbf) - " + e.getMessage() + " - " + e.getCause()); } @@ -133,7 +133,7 @@ private void checkRegisteredClaims(String payloadJson) { long iatUT = getDateJSONValue(iat); java.util.Date time = new java.util.Date(iatUT * 1000); String iatDate = time.toString(); - timeClaimList.add(new TimeClaim("[iat] Issued at ", iatDate, iatUT)); + timeClaimList.add(new TimeClaim("[iat] Issued at ", iatDate, iatUT,true,false)); } catch (Exception e) { Output.output("Could not parse claim (iat) - " + e.getMessage() + " - " + e.getCause()); } diff --git a/src/main/java/model/JWTInterceptModel.java b/src/main/java/model/JWTInterceptModel.java new file mode 100644 index 0000000..a144bbf --- /dev/null +++ b/src/main/java/model/JWTInterceptModel.java @@ -0,0 +1,72 @@ +package model; + +import java.util.List; + +import app.helpers.CookieFlagWrapper; + +public class JWTInterceptModel { + + private String jwtSignatureKey; + private CustomJWToken jwToken; + private String originalJWT; + private String problemDetail; + private CookieFlagWrapper cFW; + private List tcl; + private CustomJWToken originalJWToken; + + public String getJWTKey() { + return jwtSignatureKey; + } + + public void setJWTSignatureKey(String jwtSignatureKey) { + this.jwtSignatureKey = jwtSignatureKey; + } + + public String getProblemDetail() { + return problemDetail; + } + + public void setProblemDetail(String problemDetail) { + this.problemDetail = problemDetail; + } + + public CookieFlagWrapper getcFW() { + return cFW; + } + + public void setcFW(CookieFlagWrapper cFW) { + this.cFW = cFW; + } + + public void setTimeClaims(List tcl) { + this.tcl = tcl; + } + + public String getTimeClaimsAsText() { + return TimeClaim.getTimeClaimsAsHTML(tcl); + } + + public CustomJWToken getJwToken() { + return jwToken; + } + + public void setJwToken(CustomJWToken jwToken) { + this.jwToken = jwToken; + } + + public void setOriginalJWT(String originalJWT) { + this.originalJWT = originalJWT; + } + + public String getOriginalJWT() { + return originalJWT; + } + + public void setOriginalJWToken(CustomJWToken originalJWToken) { + this.originalJWToken = originalJWToken; + } + + public CustomJWToken getOriginalJWToken() { + return originalJWToken; + } +} \ No newline at end of file diff --git a/src/main/java/model/JWTSuiteTabModel.java b/src/main/java/model/JWTSuiteTabModel.java new file mode 100644 index 0000000..5d88f8c --- /dev/null +++ b/src/main/java/model/JWTSuiteTabModel.java @@ -0,0 +1,71 @@ +package model; + +import java.awt.Color; +import java.util.List; + +public class JWTSuiteTabModel { + + private String jwtInput; + private String jwtKey; + private Color jwtSignatureColor; + private String jwtJSON; + private String verificationLabel; + private List tcl; + private String verificationResult; + + public String getJwtInput() { + return jwtInput; + } + + public void setJwtInput(String jwtInput) { + this.jwtInput = jwtInput; + } + + public String getJwtKey() { + return jwtKey; + } + + public void setJwtKey(String jwtKey) { + this.jwtKey = jwtKey; + } + + public Color getJwtSignatureColor() { + return jwtSignatureColor; + } + + public void setJwtSignatureColor(Color jwtSignatureColor) { + this.jwtSignatureColor = jwtSignatureColor; + } + + public String getJwtJSON() { + return jwtJSON; + } + + public void setJwtJSON(String jwtJSON) { + this.jwtJSON = jwtJSON; + } + + public void setVerificationLabel(String label) { + this.verificationLabel = label; + } + + public void setVerificationResult(String result) { + this.verificationResult = result; + } + + public String getVerificationLabel() { + return this.verificationLabel; + } + + public void setTimeClaims(List tcl) { + this.tcl = tcl; + } + + public String getTimeClaimsAsText() { + return TimeClaim.getTimeClaimsAsHTML(tcl); + } + + public String getVerificationResult() { + return this.verificationResult; + } +} \ No newline at end of file diff --git a/src/main/java/model/JWTTabModel.java b/src/main/java/model/JWTTabModel.java new file mode 100644 index 0000000..80e9e5e --- /dev/null +++ b/src/main/java/model/JWTTabModel.java @@ -0,0 +1,109 @@ +package model; + +import java.awt.Color; +import java.util.List; + +import app.helpers.CookieFlagWrapper; + +public class JWTTabModel { + + private String key = ""; + private String keyLabel = ""; + private int hashCode; + private String verificationLabel = ""; + private Color verificationColor; + private String jwt; + private String jwtJSON; + private CookieFlagWrapper cFW; + private List tcl; + + public JWTTabModel() { + } + + public JWTTabModel(String keyValue, byte[] content) { + this.key = keyValue; + this.hashCode = new String(content).hashCode(); + this.verificationColor = Settings.COLOR_UNDEFINED; + } + + @Override + public boolean equals(Object otherObj) { + if (otherObj instanceof JWTTabModel otherViewState) { + return (otherViewState.getHashCode() == this.getHashCode()); + } + return false; + } + + public String getKey() { + return key; + } + + public int getHashCode() { + return hashCode; + } + + public void setKeyValueAndHash(String keyValue, int hashCode) { + this.key = keyValue; + this.hashCode = hashCode; + } + + public void setVerificationResult(String verificationResult) { + this.verificationLabel = verificationResult; + } + + public String getKeyLabel() { + return keyLabel; + } + + public String getVerificationLabel() { + return key.isEmpty() ? "" : verificationLabel; + } + + public void setVerificationLabel(String verificationLabel) { + this.verificationLabel = verificationLabel; + } + + public Color getVerificationColor() { + return key.isEmpty() ? Settings.COLOR_UNDEFINED : verificationColor; + } + + public void setVerificationColor(Color verificationColor) { + this.verificationColor = verificationColor; + } + + public void setKey(String key) { + this.key = key; + } + + public void setJWT(String token) { + this.jwt = token; + } + + public String getJWT() { + return jwt; + } + + public String getJWTJSON() { + return jwtJSON; + } + + public void setJWTJSON(String readableFormat) { + jwtJSON = readableFormat; + } + + public void setcFW(CookieFlagWrapper cFW) { + this.cFW = cFW; + } + + public CookieFlagWrapper getcFW() { + return cFW; + } + + public void setTimeClaims(List tcl) { + this.tcl = tcl; + } + + public String getTimeClaimsAsText() { + return TimeClaim.getTimeClaimsAsHTML(tcl); + } +} diff --git a/src/model/Settings.java b/src/main/java/model/Settings.java similarity index 100% rename from src/model/Settings.java rename to src/main/java/model/Settings.java diff --git a/src/main/java/model/Strings.java b/src/main/java/model/Strings.java new file mode 100644 index 0000000..e903b73 --- /dev/null +++ b/src/main/java/model/Strings.java @@ -0,0 +1,64 @@ +package model; + +import java.io.BufferedReader; +import java.io.FileReader; +import java.io.IOException; + +public class Strings { + + private Strings() { + } + + public static final String CONTEXT_MENU_STRING = "Send selected text to JSON Web Tokens Tab to decode"; + + public static final String ORIGINAL_TOKEN_STATE = "Original"; + public static final String UPDATED_TOKEN_STATE = "Token updated"; + + public static final String ACCEPT_CHANGES = "Accept Changes"; + public static final String RECALC_SIGNATURE = "Recalculate Signature"; + public static final String ORIGINAL_TOKEN = "Original Token"; + public static final String UPDATE_ALGO_SIG = "Update Algorithm / Signature"; + public static final String NO_SECRET_PROVIDED = "No secret provided"; + public static final String DECODED_JWT = "Decoded JWT"; + public static final String ENTER_JWT = "Enter JWT"; + + public static final String VALID_VERFICIATION = "Signature verified"; + public static final String INVALID_KEY_VERIFICATION = "Invalid Key"; + public static final String INVALID_SIGNATURE_VERIFICATION = "Cannot verify Signature"; + public static final String INVALID_CLAIM_VERIFICATION = "Not all Claims accepted"; + public static final String GENERIC_ERROR_VERIFICATION = "Invalid Signature / wrong key / claim failed"; + + public static final String RECALC_KEY_INTERCEPT = "Secret / Key for Signature recalculation:"; + + public static final String DONT_MODIFY = "Do not automatically modify signature"; + public static final String KEEP_ORIG_SIG = "Keep original signature"; + public static final String RANDOM_KEY = "Sign with random key pair"; + public static final String ENTER_SECRET_KEY = "Enter Secret / Key"; + public static final String CHOOSE_SIG = "Load Secret / Key from File"; + + public static final String DONT_MODIFY_TT = "The signature will be taken straight out of the editable field to the left"; + public static final String RECALC_SIG_TT = "The signature will be recalculated depending
on the content and algorithm set"; + public static final String KEEP_ORIG_SIG_TT = "The signature originally sent will be preserved and sent unchanged"; + public static final String RANDOM_KEY_TT = "The signature will be recalculated depending
on the content and algorithm set
by a random signature / key"; + public static final String CHOOSE_SIG_TT = "Load the secret / key from a file chosen by your OS file picker"; + + public static final String CREDIT_TITLE = "JSON Web Tokens - About"; + + public static final String JWT_HEADER_PREFIX = "JWT4B"; + public static final String JWT_HEADER_INFO = "The following headers are added automatically, in order to log the keys"; + + public static String filePathToString(String filePath) { + StringBuilder contentBuilder = new StringBuilder(); + try (BufferedReader br = new BufferedReader(new FileReader(filePath))) { + + String sCurrentLine; + while ((sCurrentLine = br.readLine()) != null) { + contentBuilder.append(sCurrentLine).append(System.lineSeparator()); + } + } catch (IOException e) { + e.printStackTrace(); + } + String result = contentBuilder.toString(); + return result.substring(0, result.length() - System.lineSeparator().length()); + } +} diff --git a/src/main/java/model/TimeClaim.java b/src/main/java/model/TimeClaim.java new file mode 100644 index 0000000..08d70e1 --- /dev/null +++ b/src/main/java/model/TimeClaim.java @@ -0,0 +1,30 @@ +package model; + +import java.util.List; + +import lombok.Data; +import lombok.RequiredArgsConstructor; + +@Data +@RequiredArgsConstructor +public class TimeClaim { + + private final String claim; + private final String date; + private final long unixTimestamp; + private final boolean valid; + private final boolean canBeValid; + + public static String getTimeClaimsAsHTML(List tcl) { + StringBuilder timeClaimSB = new StringBuilder(); + timeClaimSB.append(""); + if (tcl != null && !tcl.isEmpty()) { + for (TimeClaim timeClaim : tcl) { + String resultString = timeClaim.isValid() ? "passed" : "failed"; + timeClaimSB.append("" + timeClaim.getClaim() + (timeClaim.isCanBeValid() ? " check " + resultString : "") + " - " + timeClaim.getDate() + "
"); + } + } + timeClaimSB.append(""); + return timeClaimSB.toString(); + } +} diff --git a/src/model/JWTInterceptModel.java b/src/model/JWTInterceptModel.java deleted file mode 100644 index d2c732b..0000000 --- a/src/model/JWTInterceptModel.java +++ /dev/null @@ -1,72 +0,0 @@ -package model; - -import java.util.List; - -import app.helpers.CookieFlagWrapper; - -public class JWTInterceptModel { - - private String jwtSignatureKey; - private CustomJWToken jwToken; - private String originalJWT; - private String problemDetail; - private CookieFlagWrapper cFW; - private List tcl; - private CustomJWToken originalJWToken; - - public String getJWTKey() { - return jwtSignatureKey; - } - - public void setJWTSignatureKey(String jwtSignatureKey) { - this.jwtSignatureKey = jwtSignatureKey; - } - - public String getProblemDetail() { - return problemDetail; - } - - public void setProblemDetail(String problemDetail) { - this.problemDetail = problemDetail; - } - - public CookieFlagWrapper getcFW() { - return cFW; - } - - public void setcFW(CookieFlagWrapper cFW) { - this.cFW = cFW; - } - - public void setTimeClaims(List tcl) { - this.tcl = tcl; - } - - public String getTimeClaimsAsText() { - return TimeClaim.getTimeClaimsAsHTML(tcl); - } - - public CustomJWToken getJwToken() { - return jwToken; - } - - public void setJwToken(CustomJWToken jwToken) { - this.jwToken = jwToken; - } - - public void setOriginalJWT(String originalJWT) { - this.originalJWT = originalJWT; - } - - public String getOriginalJWT() { - return originalJWT; - } - - public void setOriginalJWToken(CustomJWToken originalJWToken) { - this.originalJWToken = originalJWToken; - } - - public CustomJWToken getOriginalJWToken() { - return originalJWToken; - } -} \ No newline at end of file diff --git a/src/model/JWTSuiteTabModel.java b/src/model/JWTSuiteTabModel.java deleted file mode 100644 index a355d07..0000000 --- a/src/model/JWTSuiteTabModel.java +++ /dev/null @@ -1,71 +0,0 @@ -package model; - -import java.awt.Color; -import java.util.List; - -public class JWTSuiteTabModel { - - private String jwtInput; - private String jwtKey; - private Color jwtSignatureColor; - private String jwtJSON; - private String verificationLabel; - private List tcl; - private String verificationResult; - - public String getJwtInput() { - return jwtInput; - } - - public void setJwtInput(String jwtInput) { - this.jwtInput = jwtInput; - } - - public String getJwtKey() { - return jwtKey; - } - - public void setJwtKey(String jwtKey) { - this.jwtKey = jwtKey; - } - - public Color getJwtSignatureColor() { - return jwtSignatureColor; - } - - public void setJwtSignatureColor(Color jwtSignatureColor) { - this.jwtSignatureColor = jwtSignatureColor; - } - - public String getJwtJSON() { - return jwtJSON; - } - - public void setJwtJSON(String jwtJSON) { - this.jwtJSON = jwtJSON; - } - - public void setVerificationLabel(String label) { - this.verificationLabel = label; - } - - public void setVerificationResult(String result) { - this.verificationResult = result; - } - - public String getVerificationLabel() { - return this.verificationLabel; - } - - public void setTimeClaims(List tcl) { - this.tcl = tcl; - } - - public String getTimeClaimsAsText() { - return TimeClaim.getTimeClaimsAsHTML(tcl); - } - - public String getVerificationResult() { - return this.verificationResult; - } -} \ No newline at end of file diff --git a/src/model/JWTTabModel.java b/src/model/JWTTabModel.java deleted file mode 100644 index ede6440..0000000 --- a/src/model/JWTTabModel.java +++ /dev/null @@ -1,110 +0,0 @@ -package model; - -import java.awt.Color; -import java.util.List; - -import app.helpers.CookieFlagWrapper; - -public class JWTTabModel { - - private String key = ""; - private String keyLabel = ""; - private int hashCode; - private String verificationLabel = ""; - private Color verificationColor; - private String jwt; - private String jwtJSON; - private CookieFlagWrapper cFW; - private List tcl; - - public JWTTabModel() { - } - - public JWTTabModel(String keyValue, byte[] content) { - this.key = keyValue; - this.hashCode = new String(content).hashCode(); - this.verificationColor = Settings.COLOR_UNDEFINED; - } - - @Override - public boolean equals(Object otherObj) { - if (otherObj instanceof JWTTabModel) { - JWTTabModel otherViewState = (JWTTabModel) otherObj; - return (otherViewState.getHashCode() == this.getHashCode()); - } - return false; - } - - public String getKey() { - return key; - } - - public int getHashCode() { - return hashCode; - } - - public void setKeyValueAndHash(String keyValue, int hashCode) { - this.key = keyValue; - this.hashCode = hashCode; - } - - public void setVerificationResult(String verificationResult) { - this.verificationLabel = verificationResult; - } - - public String getKeyLabel() { - return keyLabel; - } - - public String getVerificationLabel() { - return key.equals("") ? "" : verificationLabel; - } - - public void setVerificationLabel(String verificationLabel) { - this.verificationLabel = verificationLabel; - } - - public Color getVerificationColor() { - return key.equals("") ? Settings.COLOR_UNDEFINED : verificationColor; - } - - public void setVerificationColor(Color verificationColor) { - this.verificationColor = verificationColor; - } - - public void setKey(String key) { - this.key = key; - } - - public void setJWT(String token) { - this.jwt = token; - } - - public String getJWT() { - return jwt; - } - - public String getJWTJSON() { - return jwtJSON; - } - - public void setJWTJSON(String readableFormat) { - jwtJSON = readableFormat; - } - - public void setcFW(CookieFlagWrapper cFW) { - this.cFW = cFW; - } - - public CookieFlagWrapper getcFW() { - return cFW; - } - - public void setTimeClaims(List tcl) { - this.tcl = tcl; - } - - public String getTimeClaimsAsText() { - return TimeClaim.getTimeClaimsAsHTML(tcl); - } -} diff --git a/src/model/Strings.java b/src/model/Strings.java deleted file mode 100644 index 482c624..0000000 --- a/src/model/Strings.java +++ /dev/null @@ -1,64 +0,0 @@ -package model; - -import java.io.BufferedReader; -import java.io.FileReader; -import java.io.IOException; - -public class Strings { - - public static final String contextMenuString = "Send selected text to JSON Web Tokens Tab to decode"; - - public static final String tokenStateOriginal = "Original"; - public static final String tokenStateUpdated = "Token updated"; - - public static final String acceptChanges = "Accept Changes"; - public static final String recalculateSignature = "Recalculate Signature"; - public static final String originalToken = "Original Token"; - public static final String updateAlgorithmSignature = "Update Algorithm / Signature"; - public static final String noSecretsProvided = "No secret provided"; - public static final String decodedJWT = "Decoded JWT"; - public static final String enterJWT = "Enter JWT"; - - public static final String verificationValid = "Signature verified"; - public static final String verificationInvalidKey = "Invalid Key"; - public static final String verificationInvalidSignature = "Cannot verify Signature"; - public static final String verificationInvalidClaim = "Not all Claims accepted"; - public static final String verificationError = "Invalid Signature / wrong key / claim failed"; - - public static final String interceptRecalculationKey = "Secret / Key for Signature recalculation:"; - - public static final String dontModify = "Do not automatically modify signature"; - public static final String keepOriginalSignature = "Keep original signature"; - public static final String randomKey = "Sign with random key pair"; - public static final String enterSecretKey = "Enter Secret / Key"; - public static final String chooseSignature = "Load Secret / Key from File"; - - - public static final String dontModifyToolTip = "The signature will be taken straight out of the editable field to the left"; - public static final String recalculateSignatureToolTip = "The signature will be recalculated depending
on the content and algorithm set"; - public static final String keepOriginalSignatureToolTip = "The signature originally sent will be preserved and sent unchanged"; - public static final String randomKeyToolTip = "The signature will be recalculated depending
on the content and algorithm set
by a random signature / key"; - public static String chooseSignatureToolTip = "Load the secret / key from a file chosen by your OS file picker"; - - public static final String creditTitle = "JSON Web Tokens - About"; - - public static final String JWTHeaderPrefix = "JWT4B: "; - public static final String JWTHeaderInfo = - JWTHeaderPrefix + "The following headers are added automatically, in order to log the keys"; - - - public static String filePathToString(String filePath) { - StringBuilder contentBuilder = new StringBuilder(); - try (BufferedReader br = new BufferedReader(new FileReader(filePath))) { - - String sCurrentLine; - while ((sCurrentLine = br.readLine()) != null) { - contentBuilder.append(sCurrentLine).append(System.lineSeparator()); - } - } catch (IOException e) { - e.printStackTrace(); - } - String result = contentBuilder.toString(); - return result.substring(0, result.length() - System.lineSeparator().length()); - } -} diff --git a/src/model/TimeClaim.java b/src/model/TimeClaim.java deleted file mode 100644 index 03381a0..0000000 --- a/src/model/TimeClaim.java +++ /dev/null @@ -1,60 +0,0 @@ -package model; - -import java.util.List; - -public class TimeClaim { - - private final String date; - private final long unixTimestamp; - private final boolean valid; - private final String claim; - private final boolean canBeValid; - - public TimeClaim(String claim, String date, long unixTimestamp, boolean valid) { - this.claim = claim; - this.date = date; - this.unixTimestamp = unixTimestamp; - this.valid = valid; - this.canBeValid = true; - } - - public TimeClaim(String claim, String date, long unixTimestamp) { - this.claim = claim; - this.date = date; - this.unixTimestamp = unixTimestamp; - this.valid = true; - this.canBeValid = false; - } - - public String getClaimName() { - return claim; - } - - public String getDate() { - return date; - } - - public boolean canBeValid() { - return canBeValid; - } - - public boolean isValid() { - return valid; - } - - public static String getTimeClaimsAsHTML(List tcl) { - StringBuilder timeClaimSB = new StringBuilder(); - timeClaimSB.append(""); - if (tcl != null && !tcl.isEmpty()) { - for (TimeClaim timeClaim : tcl) { - timeClaimSB.append("" + timeClaim.getClaimName() + (timeClaim.canBeValid() ? - " check " + (timeClaim.isValid() ? - "passed" : - "failed") : - "") + " - " + timeClaim.getDate() + "
"); - } - } - timeClaimSB.append(""); - return timeClaimSB.toString(); - } -} diff --git a/src/module-info.java b/src/module-info.java deleted file mode 100644 index be1d022..0000000 --- a/src/module-info.java +++ /dev/null @@ -1,16 +0,0 @@ -/** - * - */ -/** - * - */ -module JWT4B { - requires java.jwt; - requires org.apache.commons.codec; - requires minimal.json; - requires com.fasterxml.jackson.core; - requires com.fasterxml.jackson.databind; - requires java.desktop; - requires commons.lang; - requires rsyntaxtextarea; -} \ No newline at end of file diff --git a/test/app/TestAlgorithmWrapper.java b/src/test/java/app/TestAlgorithmWrapper.java similarity index 84% rename from test/app/TestAlgorithmWrapper.java rename to src/test/java/app/TestAlgorithmWrapper.java index 740239b..b2a13b3 100644 --- a/test/app/TestAlgorithmWrapper.java +++ b/src/test/java/app/TestAlgorithmWrapper.java @@ -12,7 +12,7 @@ import java.util.stream.Stream; -import static app.TestTokens.*; +import static app.TestConstants.*; import static org.assertj.core.api.Assertions.assertThat; import static org.junit.jupiter.api.Assertions.assertThrows; import static org.junit.jupiter.params.provider.Arguments.arguments; @@ -20,10 +20,7 @@ class TestAlgorithmWrapper { static Stream tokensAndValidKeys() { - return Stream.of( - arguments("HS256", HS256_TOKEN, "secret"), - arguments("ES256", ES256_TOKEN, ES256_TOKEN_PUB) - ); + return Stream.of(arguments("HS256", HS256_TOKEN, "secret"), arguments("ES256", ES256_TOKEN, ES256_TOKEN_PUB)); } @MethodSource("tokensAndValidKeys") @@ -38,10 +35,7 @@ void testTokenWithProperKey(String type, String token, String key) throws Illega } static Stream tokensAndInvalidKeys() { - return Stream.of( - arguments("HS256", HS256_TOKEN, "invalid"), - arguments("ES256", ES256_TOKEN, ES256_TOKEN_PUB.replace("Z", "Y")) - ); + return Stream.of(arguments("HS256", HS256_TOKEN, "invalid"), arguments("ES256", ES256_TOKEN, ES256_TOKEN_PUB.replace("Z", "Y"))); } @MethodSource("tokensAndInvalidKeys") diff --git a/src/test/java/app/TestAuthorizationDetection.java b/src/test/java/app/TestAuthorizationDetection.java new file mode 100644 index 0000000..a5bfb4d --- /dev/null +++ b/src/test/java/app/TestAuthorizationDetection.java @@ -0,0 +1,72 @@ +package app; + +import static burp.api.montoya.http.message.requests.HttpRequest.httpRequest; + +import static app.TestConstants.*; + +import static org.assertj.core.api.Assertions.assertThat; + +import burp.api.montoya.MontoyaExtension; +import burp.api.montoya.http.message.requests.HttpRequest; + +import app.tokenposition.AuthorizationBearerHeader; + +import org.apache.commons.text.StringSubstitutor; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; + +import java.util.Map; + +@ExtendWith(MontoyaExtension.class) +class TestAuthorizationDetection { + + @Test + void testAuthValid() { + Map params = Map.of("ADD_HEADER", "Authorization: Bearer " + HS256_TOKEN); + HttpRequest httpRequest = httpRequest(StringSubstitutor.replace(REQUEST_TEMPLATE, params)); + AuthorizationBearerHeader abh = new AuthorizationBearerHeader(httpRequest, true); + + assertThat(abh.positionFound()).isTrue(); + assertThat(abh.getToken()).isEqualTo(HS256_TOKEN); + } + + @Test + void testAuthValidLowerCase() { + Map params = Map.of("ADD_HEADER", "authorization: bearer " + HS256_TOKEN); + HttpRequest httpRequest = httpRequest(StringSubstitutor.replace(REQUEST_TEMPLATE, params)); + AuthorizationBearerHeader abh = new AuthorizationBearerHeader(httpRequest, true); + + assertThat(abh.positionFound()).isTrue(); + assertThat(abh.getToken()).isEqualTo(HS256_TOKEN); + } + + @Test + void testAuthValidNonAuthHeader() { + Map params = Map.of("ADD_HEADER", "X-AUTH: bearer " + HS256_TOKEN); + HttpRequest httpRequest = httpRequest(StringSubstitutor.replace(REQUEST_TEMPLATE, params)); + AuthorizationBearerHeader abh = new AuthorizationBearerHeader(httpRequest, true); + + assertThat(abh.positionFound()).isTrue(); + assertThat(abh.getToken()).isEqualTo(HS256_TOKEN); + } + + + @Test + void testAuthInvalid() { + Map params = Map.of("ADD_HEADER", "Authorization: Bearer " + INVALID_HEADER_TOKEN); + HttpRequest httpRequest = httpRequest(StringSubstitutor.replace(REQUEST_TEMPLATE, params)); + AuthorizationBearerHeader abh = new AuthorizationBearerHeader(httpRequest, true); + + assertThat(abh.positionFound()).isFalse(); + } + + @Test + void testAuthInvalid2() { + Map params = Map.of("ADD_HEADER", "Authorization: Bearer topsecret123456789!"); + HttpRequest httpRequest = httpRequest(StringSubstitutor.replace(REQUEST_TEMPLATE, params)); + AuthorizationBearerHeader abh = new AuthorizationBearerHeader(httpRequest, true); + + assertThat(abh.positionFound()).isFalse(); + } + +} diff --git a/src/test/java/app/TestBodyDetection.java b/src/test/java/app/TestBodyDetection.java new file mode 100644 index 0000000..9908a49 --- /dev/null +++ b/src/test/java/app/TestBodyDetection.java @@ -0,0 +1,141 @@ +package app; + +import app.tokenposition.Body; +import burp.api.montoya.MontoyaExtension; +import burp.api.montoya.http.message.requests.HttpRequest; +import org.apache.commons.text.StringSubstitutor; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; + +import java.util.Map; +import java.util.stream.Stream; + +import static app.TestConstants.*; +import static burp.api.montoya.http.message.requests.HttpRequest.httpRequest; +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.params.provider.Arguments.arguments; + +@ExtendWith(MontoyaExtension.class) +class TestBodyDetection { + + static Stream bodyDataAndDetectedTokens() { + return Stream.of( + arguments("test=best&abc=" + HS256_TOKEN, HS256_TOKEN), + arguments("a " + HS256_TOKEN + " b", HS256_TOKEN), + arguments("{\"aaaa\":\"" + HS256_TOKEN + "\"}", HS256_TOKEN), + arguments("{ \"bbbb\" : { \" cccc \": { \" dddd\":\"" + HS256_TOKEN + " \"}}}", HS256_TOKEN), + arguments("token=" + HS256_TOKEN, HS256_TOKEN), + arguments(HS256_TOKEN, HS256_TOKEN), + arguments("egg=" + HS256_TOKEN + "&test=best", HS256_TOKEN), + arguments("abc def " + HS256_TOKEN + " ghi jkl", HS256_TOKEN) + ); + } + + @MethodSource("bodyDataAndDetectedTokens") + @ParameterizedTest + void testBodyDetection(String body, String bodyToken) { + Map params = Map.of( + "METHOD", METHOD_POST, + "BODY", body); + + HttpRequest httpRequest = httpRequest(StringSubstitutor.replace(REQUEST_TEMPLATE, params)); + + Body pb = new Body(httpRequest, true); + + assertThat(pb.positionFound()).isTrue(); + assertThat(pb.getToken()).isEqualTo(bodyToken); + } + + static Stream bodyDataWhereNoTokenDetected() { + return Stream.of( + arguments("{ \"bbbb\" : { \" cccc \": { \" dddd\":" + HS256_TOKEN + " \"}}}"), + arguments("{}"), + arguments("{ \"abc\": \"def\"}"), + arguments("{ \"abc\": {\"def\" : \"ghi\"} }"), + arguments("yo=" + INVALID_HEADER_TOKEN + "&test=best"), + arguments("ab " + INVALID_HEADER_TOKEN + " de"), + arguments(HS256_TOKEN + "&"), + arguments("abc def ghi jkl"), + arguments("") + ); + } + + @MethodSource("bodyDataWhereNoTokenDetected") + @ParameterizedTest + void testBodyDetection(String body) { + Map params = Map.of( + "METHOD", METHOD_POST, + "BODY", body); + + HttpRequest httpRequest = httpRequest(StringSubstitutor.replace(REQUEST_TEMPLATE, params)); + + Body pb = new Body(httpRequest, true); + + assertThat(pb.positionFound()).isFalse(); + } + + + @Test + void testPostBodyReplaceWithParam() { + String body1 = "test=best&token=" + HS256_TOKEN; + + Map params1 = Map.of( + "METHOD", METHOD_POST, + "BODY", body1); + + HttpRequest httpRequest1 = httpRequest(StringSubstitutor.replace(REQUEST_TEMPLATE, params1)); + + Body pb1 = new Body(httpRequest1, true); + pb1.positionFound(); + pb1.replaceToken(HS256_TOKEN_2); + + // + String body2 = "test=best&token=" + HS256_TOKEN_2; + + Map params2 = Map.of( + "METHOD", METHOD_POST, + "BODY", body2); + + HttpRequest httpRequest2 = httpRequest(StringSubstitutor.replace(REQUEST_TEMPLATE, params2)); + + Body pb2 = new Body(httpRequest2, true); + pb2.positionFound(); + + // + assertThat(pb1.getRequest().toString()).isEqualTo(pb2.getRequest().toString()); + } + + + @Test + void testPostBodyReplaceWithoutParam() { + String body1 = "ab\n cd " + HS256_TOKEN + " cd"; + + Map params1 = Map.of( + "METHOD", METHOD_POST, + "BODY", body1); + + HttpRequest httpRequest1 = httpRequest(StringSubstitutor.replace(REQUEST_TEMPLATE, params1)); + + Body pb1 = new Body(httpRequest1, true); + pb1.positionFound(); + pb1.replaceToken(HS256_TOKEN_2); + + // + String body2 = "ab\n cd " + HS256_TOKEN_2 + " cd"; + + Map params2 = Map.of( + "METHOD", METHOD_POST, + "BODY", body2); + + HttpRequest httpRequest2 = httpRequest(StringSubstitutor.replace(REQUEST_TEMPLATE, params2)); + + Body pb2 = new Body(httpRequest2, true); + pb2.positionFound(); + + // + assertThat(pb1.getRequest().toString()).isEqualTo(pb2.getRequest().toString()); + } +} diff --git a/src/test/java/app/TestConstants.java b/src/test/java/app/TestConstants.java new file mode 100644 index 0000000..ffcaa54 --- /dev/null +++ b/src/test/java/app/TestConstants.java @@ -0,0 +1,51 @@ +package app; + +public class TestConstants { + private TestConstants() {} + + public static final String METHOD_POST = "POST"; + public static final String METHOD_GET = "GET"; + + public static final String REQUEST_TEMPLATE = """ + ${METHOD:-GET} / HTTP/1.1\r + Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8\r + Accept-Language: en-US,en;q=0.5\r${ADD_HEADER:-} + Connection: close\r + Upgrade-Insecure-Requests: 1\r + \r + ${BODY:-}"""; + + public static final String RESPONSE_TEMPLATE = """ + HTTP/1.1 200 OK\r + Connection: Keep-Alive\r + Content-Type: text/html; charset=utf-8\r + Date: Wed, 10 Aug 2026 13:17:18 GMT\r${ADD_HEADER:-} + Server: Apache\r + \r + ${BODY:-}"""; + + public static final String HS256_HEADER = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9"; + public static final String RS256_HEADER = "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9"; + public static final String ES256_HEADER = "eyJhbGciOiJFUzI1NiIsInR5cCI6IkpXVCJ9"; + + public static final String HS256_TOKEN = HS256_HEADER + ".eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWV9.TJVA95OrM7E2cBab30RMHrHDcEfxjoYZgeFONFh7HgQ"; + public static final String HS256_BEAUTIFIED_TOKEN = HS256_HEADER + ".ewogICAic3ViIjoiMTIzNDU2Nzg5MCIsCiAgICJuYW1lIjoiSm9obiBEb2UiLAogICAiYWRtaW4iOnRydWUKfQ==.TJVA95OrM7E2cBab30RMHrHDcEfxjoYZgeFONFh7HgQ"; + public static final String HS256_TOKEN_2 = HS256_HEADER + ".eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6Ik1heCBNdXN0ZXJsaSIsImFkbWluIjp0cnVlfQ.9o7iXB3CEm8ciIJjc_yZPI49p7gSKX6zDddr3Gp5_hU"; + + public static final String INVALID_HEADER_TOKEN = "eyJhbFbiOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWV9.TJVA95OrM7E2cBab30RMHrHDcEfxjYZgeFONFh7HgQ"; + public static final String INVALID_HEADER_TOKEN_2 = "eyJhbFb___RANDOM_GARBAGE___ZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWV9.TJVA95OrM7E2cBab30RMHrHDcEfxjYZgeFONFh7HgQ"; + public static final String INVALID_JSON_TOKEN = "eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDdIyfQ.GuoUe6tw79bJlbU1HU0ADX0pr0u2kf3r_4OdrDufSfQ"; + + public static final String ES256_TOKEN = ES256_HEADER + ".eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWUsImlhdCI6MTUxNjIzOTAyMn0.tyh-VfuzIxCyGYDlkBA7DfyjrqmSHu6pQ2hoZuFqUSLPNY2N0mpHb3nk5K17HWP_3cYHBw7AhHale5wky6-sVA"; + public static final String ES256_TOKEN_PUB = "-----BEGIN PUBLIC KEY-----MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEEVs/o5+uQbTjL3chynL4wXgUg2R9q9UU8I5mEovUf86QZ7kOBIjJwqnzD1omageEHWwHdBO6B+dFabmdT9POxg==-----END PUBLIC KEY-----"; + + // How To - CLI + // echo -n '{"alg":"RS256","typ":"JWT"}' | base64 | sed s/\+/-/ | sed -E s/=+$// + // echo -n '{"sub":"RS256inOTA","name":"John Doe"}' | base64 | sed s/\+/-/ | sed -E s/=+$// + // openssl genrsa 2048 > jwtRSA256-private.pem + // openssl rsa -in jwtRSA256-private.pem -pubout -outform PEM -out jwtRSA256-public.pem + // echo -n "eyJhbGciOiJFUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJFUzI1NmluT1RBIiwibmFtZSI6IkpvaG4gRG9lIn0" | openssl dgst -sha256 -binary -sign jwtRSA256-private.pem | openssl enc -base64 | tr -d '\n=' | tr -- '+/' '-_' + public static final String RS256_TOKEN = RS256_HEADER + ".eyJzdWIiOiJSUzI1NmluT1RBIiwibmFtZSI6IkpvaG4gRG9lIn0.VnF6UI5CHgOcg4T-k04xWLy5DW_-BiH75ccS9EpF1KP-5QAPKSqhls558cSa2DBPj5yeoFql9DFZ9H_mthbtz_HSfQ1DEDviP5mVfx9c5scEE9ebCaz9a5fQ_2uS2urh6HFTV7kGzjRqKJOCmB6gqtgGsPioDtrWU4o9mlqCh7k3meKTk5AJjeULgts96H2or4P9SUPXmI4Bv97bfSoj8LD3aHgI5FeKBU1KBEDFgDwy3WSI-SBlkf-43EQZwMgIvSVgqY9VXkJnS2aeu76oRn1MzpJBxWVRQaBrTZRnB0CCt3JjtK1QtIGHkl-M9-bVviQ-XtVqp52-DPG2GZFpqQ"; + public static final String RS256_TOKEN_PUB = "-----BEGIN PUBLIC KEY-----MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAss7FTpt5OpOsNbb5bfmLZnn0D7NzkxqWn4s2r3ZkPcDFMLF4/31sJHCdNkiavFaM7w+DfuSXb0rSQ1Eh/WX9UPR/BN0a8BRzogfzcXOekt4DdnLZibkYtcBfg519tbNVu6geuYi4QbwXrtJUfEAGSbvC3F11aO/qtPHJiwC5XHLgA8kteVXNgto6IBmq2bio9kKMVtceNjxGm6PnH9jBWB3cnlHYipg6hZlqfkiw8sF7UosfTqGn4ibTNUxNVNQw3K5w9S9YylaNq5HOVeHX1egz0aokkXoNwjV/31kG+SQq7MKiJ/PlCPbzY5e3++chEAg6dMKI/FOmIJIwbw1rHwIDAQAB-----END PUBLIC KEY-----"; + public static final String RS256_TOKEN_PRIV = "-----BEGIN RSA PRIVATE KEY-----MIIEpAIBAAKCAQEAss7FTpt5OpOsNbb5bfmLZnn0D7NzkxqWn4s2r3ZkPcDFMLF4/31sJHCdNkiavFaM7w+DfuSXb0rSQ1Eh/WX9UPR/BN0a8BRzogfzcXOekt4DdnLZibkYtcBfg519tbNVu6geuYi4QbwXrtJUfEAGSbvC3F11aO/qtPHJiwC5XHLgA8kteVXNgto6IBmq2bio9kKMVtceNjxGm6PnH9jBWB3cnlHYipg6hZlqfkiw8sF7UosfTqGn4ibTNUxNVNQw3K5w9S9YylaNq5HOVeHX1egz0aokkXoNwjV/31kG+SQq7MKiJ/PlCPbzY5e3++chEAg6dMKI/FOmIJIwbw1rHwIDAQABAoIBAGBQ7QtoyCZ7gVn10+ofb62lp4gFnA3zVoteS/i8B0cUXaPbFVhaUTRXzPd+qIsm/AeSDbz+mWwDm7tTKsH6fDdtXDZce7Qy8A6pxcKpCxQFr0vQlcmQAPV2SHz3Cs4jad0JtHMwaEBQd1leRtAfFMQG9fIKDcKW6ZDKZUwQ+cgH2XRFbEYtEgrw/G5+ZCwk7lENJCRVIqOGZH5ZmSrIgeEJP1sZgt1+qMDzDSiVV0EHdH07n/5SNuPawazSCa8/NOcpSyADdD3mJ6NN1icS6NAYeFS2mEecx5Wh3Dsx01E1YpdcF2m+nvuWqIlIl8DX1P9cs8fg1qJbedUXE21hg1ECgYEA7AJug9W1Xqt/fFhHCNGaILznsGCKS5FHzm6zOOHX1cH9zjXLl0Ywvv2ioX0utlOk5KxPUwxKTUvg1cvZ0WsJWckgcfkvKxZ/mfT3E56Ocf8i/Ra1R7dF0V+quXE0uNHvfQGyGqU8d6BkDXE3TFFkxzdcoBx1uvk1K8HOxsNtSBsCgYEAwfP9eWOguLFN9C/KuUbeGsRNO1rfQcPL2F8T7W5NaBKZl5f85KuG7JaSA7s4aIpSuACTGYE6AS/4AiShadjf+HXqXcTxBUWJyFoa+Py7Qfb1a4sPOQUnz/CXukiBSHXWmNqVbuKODu6ARmFoHUd0KvK4fPilOnmCfgVbjMtd4U0CgYEA6ltHztYKMiXuhFVMxG8Os++hyj0zVvK+8Thv884f+32VQI2ey2rBwQYv1lhuaFMK7KBGbNtJdRQiAWtZsmCtemEEPOkKc6j1sLXWG79ZB84oulUwUjSludFbwKWvis+9Fs72QwtNziSQ9eA03y37+u74pW1dYvtQV1EuuaUaAX0CgYEAwBFjPkbO7peG3v5E/12SrWcgJFtFI9dFkqv1C/djaGCjAWBd7AWAw+IIDvHkVoJEkDrhcSxryKk8LMMhpbRDd8UtplZVaCcI3wN8Gn4M4rIxL6KyHIFif6V+W9dZT+yB6zTrLrfkfhzposjrVbNg8vcSg4+n8FRMSYf8tVzfRzECgYAcsA/jdZthHEN6P0FycbAL4ALcCK1AcBVwdyrPjOm3sJ+7j0AoIRT9UlIyZ8xhtC3EX2iURJKlENdAnPQYThR+kCWGJq4CHhod9RhJgXzDyYYxyGcLBKTBcXjzZpx0jSguk3UobMdXgL2kG70tfwt1Y1b201OJmOLTg8sFmTYJRA==-----END RSA PRIVATE KEY-----"; +} \ No newline at end of file diff --git a/test/app/TestCookieDetection.java b/src/test/java/app/TestCookieDetection.java similarity index 62% rename from test/app/TestCookieDetection.java rename to src/test/java/app/TestCookieDetection.java index f9f59d7..61ba0c8 100644 --- a/test/app/TestCookieDetection.java +++ b/src/test/java/app/TestCookieDetection.java @@ -1,19 +1,24 @@ package app; -import java.util.ArrayList; -import java.util.List; +import java.util.Map; import java.util.stream.Stream; import app.tokenposition.Cookie; +import burp.api.montoya.MontoyaExtension; +import burp.api.montoya.http.message.requests.HttpRequest; +import org.apache.commons.text.StringSubstitutor; +import org.junit.jupiter.api.extension.ExtendWith; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.Arguments; import org.junit.jupiter.params.provider.MethodSource; -import static app.TestTokens.HS256_TOKEN; -import static app.TestTokens.INVALID_TOKEN; +import static app.TestConstants.*; +import static app.TestConstants.REQUEST_TEMPLATE; +import static burp.api.montoya.http.message.requests.HttpRequest.httpRequest; import static org.assertj.core.api.Assertions.assertThat; import static org.junit.jupiter.params.provider.Arguments.arguments; +@ExtendWith(MontoyaExtension.class) class TestCookieDetection { static Stream cookieHeaderAndToken() { @@ -26,7 +31,7 @@ static Stream cookieHeaderAndToken() { arguments("Cookie: secondcookie=4321; weirdcookie ; token=" + HS256_TOKEN + ";", HS256_TOKEN), arguments("Cookie: othercookie=1234; token=" + HS256_TOKEN, HS256_TOKEN), arguments("Cookie: token=" + HS256_TOKEN, HS256_TOKEN), - arguments("Cookie: token=" + INVALID_TOKEN, null), + arguments("Cookie: token=" + INVALID_HEADER_TOKEN, null), arguments("Cookie: test=besst", null) ); } @@ -34,17 +39,14 @@ static Stream cookieHeaderAndToken() { @MethodSource("cookieHeaderAndToken") @ParameterizedTest(name = "{0}") void testCookie(String cookieHeader, String cookieToken) { - List headers = new ArrayList<>(); - headers.add("GET /jwt/response_cookie.php HTTP/1.1"); - headers.add("Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8"); - headers.add("Accept-Language: en-US,en;q=0.5"); - headers.add(cookieHeader); - headers.add("Connection: close"); - headers.add("Upgrade-Insecure-Requests: 1"); - Cookie cookie = new Cookie(headers,""); - - String result = cookie.findJWTInHeaders(headers); - - assertThat(result).isEqualTo(cookieToken); + Map params = Map.of( + "ADD_HEADER", cookieHeader); + + HttpRequest httpRequest = httpRequest(StringSubstitutor.replace(REQUEST_TEMPLATE, params)); + + Cookie cookie = new Cookie(httpRequest,true); + + assertThat(cookie.positionFound()).isEqualTo(cookieToken != null); + assertThat(cookie.getToken()).isEqualTo((cookieToken != null) ? cookieToken : ""); } } diff --git a/src/test/java/app/TestCustomJWTDecoder.java b/src/test/java/app/TestCustomJWTDecoder.java new file mode 100644 index 0000000..164065d --- /dev/null +++ b/src/test/java/app/TestCustomJWTDecoder.java @@ -0,0 +1,35 @@ +package app; + +import model.CustomJWToken; +import org.junit.jupiter.api.Test; + +import static app.TestConstants.*; +import static org.assertj.core.api.Assertions.assertThat; + +class TestCustomJWTDecoder { + + @Test + void testIfTokenCanBeDecoded() { + CustomJWToken reConstructedToken = new CustomJWToken(HS256_TOKEN); + assertThat(reConstructedToken.getToken()).isEqualTo(HS256_TOKEN); + assertThat(reConstructedToken.isBuiltSuccessful()).isTrue(); + } + + @Test + void testBrokenToken() { + CustomJWToken reConstructedToken = new CustomJWToken(INVALID_HEADER_TOKEN); + assertThat(reConstructedToken.isBuiltSuccessful()).isFalse(); + } + + @Test + void testIfTokenIsMinified() { + CustomJWToken reConstructedToken = new CustomJWToken(HS256_TOKEN); + assertThat(reConstructedToken.isMinified()).isTrue(); + } + + @Test + void testIfTokenIsNotMinified() { + CustomJWToken reConstructedToken = new CustomJWToken(HS256_BEAUTIFIED_TOKEN); + assertThat(reConstructedToken.isMinified()).isFalse(); + } +} \ No newline at end of file diff --git a/test/app/TestInvalidJSONToken.java b/src/test/java/app/TestInvalidJSONToken.java similarity index 65% rename from test/app/TestInvalidJSONToken.java rename to src/test/java/app/TestInvalidJSONToken.java index 7948969..9dc025b 100644 --- a/test/app/TestInvalidJSONToken.java +++ b/src/test/java/app/TestInvalidJSONToken.java @@ -5,18 +5,19 @@ import model.CustomJWToken; import org.junit.jupiter.api.Test; +import static app.TestConstants.INVALID_JSON_TOKEN; import static org.junit.jupiter.api.Assertions.assertEquals; class TestInvalidJSONToken { @Test void newTest() throws IllegalArgumentException { - String token = "eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDdIyfQ.GuoUe6tw79bJlbU1HU0ADX0pr0u2kf3r_4OdrDufSfQ"; Algorithm algo = Algorithm.HMAC256("test"); - CustomJWToken cjt = new CustomJWToken(token); + CustomJWToken cjt = new CustomJWToken(INVALID_JSON_TOKEN); cjt.calculateAndSetSignature(algo); String getToken = cjt.getToken(); // we now assume that invalid tokens will be returned as received - assertEquals(getToken, getToken); + + assertEquals(getToken, getToken); // TODO } } diff --git a/test/app/TestJWTValidCheck.java b/src/test/java/app/TestJWTValidCheck.java similarity index 54% rename from test/app/TestJWTValidCheck.java rename to src/test/java/app/TestJWTValidCheck.java index 19dee72..c9f5f16 100644 --- a/test/app/TestJWTValidCheck.java +++ b/src/test/java/app/TestJWTValidCheck.java @@ -3,7 +3,7 @@ import model.CustomJWToken; import org.junit.jupiter.api.Test; -import static app.TestTokens.*; +import static app.TestConstants.*; import static org.assertj.core.api.Assertions.assertThat; class TestJWTValidCheck { @@ -14,12 +14,12 @@ void testValid() { } @Test - void testInValid() { - assertThat(CustomJWToken.isValidJWT(INVALID_TOKEN)).isFalse(); + void testInvalidHeader() { + assertThat(CustomJWToken.isValidJWT(INVALID_HEADER_TOKEN)).isFalse(); } @Test - void testInValid2() { - assertThat(CustomJWToken.isValidJWT(INVALID_TOKEN_2)).isFalse(); + void testInvalidHeader2() { + assertThat(CustomJWToken.isValidJWT(INVALID_HEADER_TOKEN_2)).isFalse(); } } diff --git a/test/app/TestKeyHelper.java b/src/test/java/app/TestKeyHelper.java similarity index 88% rename from test/app/TestKeyHelper.java rename to src/test/java/app/TestKeyHelper.java index fd04349..e6ce74c 100644 --- a/test/app/TestKeyHelper.java +++ b/src/test/java/app/TestKeyHelper.java @@ -10,7 +10,7 @@ class TestKeyHelper { - @ValueSource(strings = {"RSA", "EC"}) + @ValueSource(strings = { "RSA", "EC" }) @ParameterizedTest void testGetKeyInstanceWithNullPublicKey(String algorithm) { Key key = KeyHelper.getKeyInstance(null, algorithm, false); @@ -18,7 +18,7 @@ void testGetKeyInstanceWithNullPublicKey(String algorithm) { assertThat(key).isNull(); } - @ValueSource(strings = {"RSA", "EC"}) + @ValueSource(strings = { "RSA", "EC" }) @ParameterizedTest void testGetKeyInstanceWithNullPrivateKey(String algorithm) { Key key = KeyHelper.getKeyInstance(null, algorithm, true); diff --git a/src/test/java/app/TestPostDetection.java b/src/test/java/app/TestPostDetection.java new file mode 100644 index 0000000..14b32c1 --- /dev/null +++ b/src/test/java/app/TestPostDetection.java @@ -0,0 +1,100 @@ +package app; + + +import app.tokenposition.PostBody; +import burp.api.montoya.MontoyaExtension; +import burp.api.montoya.http.message.requests.HttpRequest; +import org.apache.commons.text.StringSubstitutor; +import org.junit.jupiter.api.extension.ExtendWith; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; +import org.junit.jupiter.api.Test; + +import java.util.Map; +import java.util.stream.Stream; + +import static app.TestConstants.*; +import static burp.api.montoya.http.message.requests.HttpRequest.httpRequest; +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.params.provider.Arguments.arguments; + +@ExtendWith(MontoyaExtension.class) +class TestPostDetection { + + static Stream postDataAndDetectedTokens() { + return Stream.of( + arguments("test=best&token=" + HS256_TOKEN, HS256_TOKEN), + arguments("token=" + HS256_TOKEN, HS256_TOKEN), + arguments("token=" + HS256_TOKEN + "&test=best", HS256_TOKEN) + ); + } + + @MethodSource("postDataAndDetectedTokens") + @ParameterizedTest + void testPostBody(String body, String bodyToken) { + Map params = Map.of( + "METHOD", METHOD_POST, + "BODY", body); + + HttpRequest httpRequest = httpRequest(StringSubstitutor.replace(REQUEST_TEMPLATE, params)); + + PostBody pb = new PostBody(httpRequest, true); + + assertThat(pb.positionFound()).isTrue(); + assertThat(pb.getToken()).isEqualTo(bodyToken); + } + + + static Stream postDataWhereNoTokenDetected() { + return Stream.of( + arguments("token=" + INVALID_HEADER_TOKEN + "&test=best"), + arguments("") + ); + } + + @MethodSource("postDataWhereNoTokenDetected") + @ParameterizedTest + void testPostBodyNoToken(String body) { + Map params = Map.of( + "METHOD", METHOD_POST, + "BODY", body); + + HttpRequest httpRequest = httpRequest(StringSubstitutor.replace(REQUEST_TEMPLATE, params)); + + PostBody pb = new PostBody(httpRequest, true); + + assertThat(pb.positionFound()).isFalse(); + assertThat(pb.getToken()).isEmpty(); + } + + @Test + void testPostBodyReplace() { + String body1 = "test=best&token=" + HS256_TOKEN; + Map params1 = Map.of( + "METHOD", METHOD_POST, + "BODY", body1); + + HttpRequest httpRequest1 = httpRequest(StringSubstitutor.replace(REQUEST_TEMPLATE, params1)); + + PostBody pb1 = new PostBody(httpRequest1, true); + pb1.positionFound(); + + pb1.replaceToken(HS256_TOKEN_2); + + // + String body2 = "test=best&token=" + HS256_TOKEN_2; + Map params2 = Map.of( + "METHOD", METHOD_POST, + "BODY", body2); + + HttpRequest httpRequest2 = httpRequest(StringSubstitutor.replace(REQUEST_TEMPLATE, params2)); + + PostBody pb2 = new PostBody(httpRequest2, true); + pb2.positionFound(); + + // + assertThat(pb1.getRequest().toString()).isEqualTo(pb2.getRequest().toString()); + } + +} diff --git a/src/test/java/app/TestSetCookieDetection.java b/src/test/java/app/TestSetCookieDetection.java new file mode 100644 index 0000000..5c4753c --- /dev/null +++ b/src/test/java/app/TestSetCookieDetection.java @@ -0,0 +1,154 @@ +package app; + +import app.tokenposition.Cookie; +import burp.api.montoya.MontoyaExtension; +import burp.api.montoya.http.message.responses.HttpResponse; +import org.apache.commons.text.StringSubstitutor; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; + +import java.util.Map; + +import static app.TestConstants.*; +import static burp.api.montoya.http.message.responses.HttpResponse.httpResponse; +import static org.assertj.core.api.Assertions.assertThat; + +@ExtendWith(MontoyaExtension.class) +class TestSetCookieDetection { + + @Test + void testCookieReversedOrder() { + String cookieHeader = "Set-Cookie: token=" + HS256_TOKEN + "\r\n" + "Set-Cookie: test=best"; + + Map params = Map.of("ADD_HEADER", cookieHeader); + + HttpResponse httpResponse = httpResponse(StringSubstitutor.replace(RESPONSE_TEMPLATE, params)); + + Cookie cookie = new Cookie(httpResponse, false); + + assertThat(cookie.positionFound()).isTrue(); + assertThat(cookie.getToken()).isEqualTo(HS256_TOKEN); + } + + @Test + void testCookieInvalidJWT() { + String cookieHeader = "Set-Cookie: token=" + INVALID_HEADER_TOKEN + "\r\n" + "Set-Cookie: test=best"; + + Map params = Map.of("ADD_HEADER", cookieHeader); + + HttpResponse httpResponse = httpResponse(StringSubstitutor.replace(RESPONSE_TEMPLATE, params)); + + Cookie cookie = new Cookie(httpResponse, false); + + assertThat(cookie.positionFound()).isFalse(); + assertThat(cookie.getToken()).isEmpty(); + } + + @Test + void testSetCookieSemicolon() { + String cookieHeader = "Set-Cookie: token=" + HS256_TOKEN + ";"; + + Map params = Map.of("ADD_HEADER", cookieHeader); + + HttpResponse httpResponse = httpResponse(StringSubstitutor.replace(RESPONSE_TEMPLATE, params)); + + Cookie cookie = new Cookie(httpResponse, false); + + assertThat(cookie.positionFound()).isTrue(); + assertThat(cookie.getToken()).isEqualTo(HS256_TOKEN); + } + + @Test + void testSetCookieReplace() { + String cookieHeader1 = "Set-Cookie: token=" + HS256_TOKEN; + + Map params1 = Map.of("ADD_HEADER", cookieHeader1); + + HttpResponse httpResponse1 = httpResponse(StringSubstitutor.replace(RESPONSE_TEMPLATE, params1)); + + Cookie cookie1 = new Cookie(httpResponse1, false); + + assertThat(cookie1.positionFound()).isTrue(); + assertThat(cookie1.getToken()).isEqualTo(HS256_TOKEN); + + cookie1.replaceToken(HS256_TOKEN_2); + + // + String cookieHeader2 = "Set-Cookie: token=" + HS256_TOKEN_2; + + Map params2 = Map.of("ADD_HEADER", cookieHeader2); + + HttpResponse httpResponse2 = httpResponse(StringSubstitutor.replace(RESPONSE_TEMPLATE, params2)); + + Cookie cookie2 = new Cookie(httpResponse2, false); + + assertThat(cookie2.positionFound()).isTrue(); + assertThat(cookie2.getToken()).isEqualTo(HS256_TOKEN_2); + + // + assertThat(cookie1.getResponse().toString()).isEqualTo(cookie2.getResponse().toString()); + } + + @Test + void testSetCookieSecureFlag() { + String cookieHeader = "Set-Cookie: token=" + HS256_TOKEN + "; Secure;"; + + Map params = Map.of("ADD_HEADER", cookieHeader); + + HttpResponse httpResponse = httpResponse(StringSubstitutor.replace(RESPONSE_TEMPLATE, params)); + + Cookie cookie = new Cookie(httpResponse, false); + + assertThat(cookie.positionFound()).isTrue(); + assertThat(cookie.getToken()).isEqualTo(HS256_TOKEN); + + assertThat(cookie.getcFW().hasSecureFlag()).isTrue(); + } + + @Test + void testSetCookieHTTPOnlyFlag() { + String cookieHeader = "Set-Cookie: token=" + HS256_TOKEN + "; expires=Thu, 01-Jan-1970 01:40:00 GMT; HttpOnly; Max-Age=0; path=/;"; + + Map params = Map.of("ADD_HEADER", cookieHeader); + + HttpResponse httpResponse = httpResponse(StringSubstitutor.replace(RESPONSE_TEMPLATE, params)); + + Cookie cookie = new Cookie(httpResponse, false); + + assertThat(cookie.positionFound()).isTrue(); + assertThat(cookie.getToken()).isEqualTo(HS256_TOKEN); + assertThat(cookie.getcFW().hasHttpOnlyFlag()).isTrue(); + } + + @Test + void testSetCookieBothFlags() { + String cookieHeader = "Set-Cookie: token=" + HS256_TOKEN + "; expires=Thu, 01-Jan-1970 01:40:00 GMT; HttpOnly; Max-Age=0; secure; path=/;"; + + Map params = Map.of("ADD_HEADER", cookieHeader); + + HttpResponse httpResponse = httpResponse(StringSubstitutor.replace(RESPONSE_TEMPLATE, params)); + + Cookie cookie = new Cookie(httpResponse, false); + + assertThat(cookie.positionFound()).isTrue(); + assertThat(cookie.getToken()).isEqualTo(HS256_TOKEN); + assertThat(cookie.getcFW().hasHttpOnlyFlag()).isTrue(); + assertThat(cookie.getcFW().hasSecureFlag()).isTrue(); + } + + @Test + void testSetCookieNoFlags() { + String cookieHeader = "Set-Cookie: token=" + HS256_TOKEN + "; expires=Thu, 01-Jan-1970 01:40:00 GMT; Max-Age=0; path=/;"; + + Map params = Map.of("ADD_HEADER", cookieHeader); + + HttpResponse httpResponse = httpResponse(StringSubstitutor.replace(RESPONSE_TEMPLATE, params)); + + Cookie cookie = new Cookie(httpResponse, false); + + assertThat(cookie.positionFound()).isTrue(); + assertThat(cookie.getToken()).isEqualTo(HS256_TOKEN); + assertThat(cookie.getcFW().hasHttpOnlyFlag()).isFalse(); + assertThat(cookie.getcFW().hasSecureFlag()).isFalse(); + } +} diff --git a/test/app/controllers/ReadableTokenFormatTest.java b/src/test/java/app/controllers/ReadableTokenFormatTest.java similarity index 100% rename from test/app/controllers/ReadableTokenFormatTest.java rename to src/test/java/app/controllers/ReadableTokenFormatTest.java diff --git a/src/test/java/burp/api/montoya/MontoyaExtension.java b/src/test/java/burp/api/montoya/MontoyaExtension.java new file mode 100644 index 0000000..42b9e67 --- /dev/null +++ b/src/test/java/burp/api/montoya/MontoyaExtension.java @@ -0,0 +1,36 @@ +package burp.api.montoya; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import org.junit.jupiter.api.extension.BeforeAllCallback; +import org.junit.jupiter.api.extension.ExtensionContext; +import org.mockito.stubbing.Answer; + +import burp.api.montoya.core.FakeHttpHeader; +import burp.api.montoya.core.FakeHttpParameter; +import burp.api.montoya.core.FakeHttpRequest; +import burp.api.montoya.core.FakeHttpResponse; +import burp.api.montoya.http.message.HttpHeader; +import burp.api.montoya.http.message.params.HttpParameter; +import burp.api.montoya.http.message.params.HttpParameterType; +import burp.api.montoya.http.message.requests.HttpRequest; +import burp.api.montoya.http.message.responses.HttpResponse; +import burp.api.montoya.internal.MontoyaObjectFactory; +import burp.api.montoya.internal.ObjectFactoryLocator; + +public class MontoyaExtension implements BeforeAllCallback { + @Override + public void beforeAll(ExtensionContext extensionContext) { + ObjectFactoryLocator.FACTORY = mock(MontoyaObjectFactory.class); + + MontoyaObjectFactory factory = ObjectFactoryLocator.FACTORY; + when(factory.httpResponse(anyString())).then((Answer) i -> new FakeHttpResponse(i.getArgument(0))); + when(factory.httpRequest(anyString())).then((Answer) i -> new FakeHttpRequest(i.getArgument(0))); + when(factory.httpHeader(anyString(), anyString())).then((Answer) i -> new FakeHttpHeader(i.getArgument(0), i.getArgument(1))); + when(factory.parameter(anyString(), anyString(), any(HttpParameterType.class))).then((Answer) i -> new FakeHttpParameter(i.getArgument(0), i.getArgument(1), i.getArgument(2))); + when(factory.bodyParameter(anyString(), anyString())).then((Answer) i -> new FakeHttpParameter(i.getArgument(0), i.getArgument(1), HttpParameterType.BODY)); + } +} \ No newline at end of file diff --git a/src/test/java/burp/api/montoya/core/FakeByteArray.java b/src/test/java/burp/api/montoya/core/FakeByteArray.java new file mode 100644 index 0000000..e500bf2 --- /dev/null +++ b/src/test/java/burp/api/montoya/core/FakeByteArray.java @@ -0,0 +1,204 @@ +// copied from https://github.com/Hannah-PortSwigger/WebSocketTurboIntruder/blob/main/src/test/java/burp/api/montoya/core/FakeByteArray.java + +package burp.api.montoya.core; + +import java.util.Iterator; +import java.util.regex.Pattern; + +import static java.nio.charset.StandardCharsets.UTF_8; + +public class FakeByteArray implements ByteArray { + private final byte[] data; + + public FakeByteArray(String data) { + this(data.getBytes(UTF_8)); + } + + public FakeByteArray(byte[] data) { + this.data = data; + } + + @Override + public byte getByte(int index) { + return data[index]; + } + + @Override + public void setByte(int index, byte value) { + data[index] = value; + } + + @Override + public void setByte(int index, int value) { + setByte(index, (byte) (0xFF & value)); + } + + @Override + public void setBytes(int index, byte... data) { + throw new UnsupportedOperationException(); + } + + @Override + public void setBytes(int index, int... data) { + throw new UnsupportedOperationException(); + } + + @Override + public void setBytes(int index, ByteArray byteArray) { + throw new UnsupportedOperationException(); + } + + @Override + public int length() { + return data.length; + } + + @Override + public byte[] getBytes() { + return data; + } + + @Override + public ByteArray subArray(int startIndexInclusive, int endIndexExclusive) { + if (startIndexInclusive < 0 || endIndexExclusive <= startIndexInclusive || endIndexExclusive > data.length) { + throw new IllegalArgumentException("Invalid indices (%d, %d) for array of length %d".formatted(startIndexInclusive, endIndexExclusive, data.length)); + } + + int length = endIndexExclusive - startIndexInclusive; + byte[] subArray = new byte[length]; + System.arraycopy(data, startIndexInclusive, subArray, 0, length); + + return new FakeByteArray(subArray); + } + + @Override + public ByteArray subArray(Range range) { + throw new UnsupportedOperationException(); + } + + @Override + public ByteArray copy() { + throw new UnsupportedOperationException(); + } + + @Override + public ByteArray copyToTempFile() { + throw new UnsupportedOperationException(); + } + + @Override + public int indexOf(ByteArray searchTerm) { + throw new UnsupportedOperationException(); + } + + @Override + public int indexOf(String searchTerm) { + throw new UnsupportedOperationException(); + } + + @Override + public int indexOf(ByteArray searchTerm, boolean caseSensitive) { + throw new UnsupportedOperationException(); + } + + @Override + public int indexOf(String searchTerm, boolean caseSensitive) { + throw new UnsupportedOperationException(); + } + + @Override + public int indexOf(ByteArray searchTerm, boolean caseSensitive, int startIndexInclusive, int endIndexExclusive) { + throw new UnsupportedOperationException(); + } + + @Override + public int indexOf(String searchTerm, boolean caseSensitive, int startIndexInclusive, int endIndexExclusive) { + throw new UnsupportedOperationException(); + } + + @Override + public int indexOf(Pattern pattern) { + throw new UnsupportedOperationException(); + } + + @Override + public int indexOf(Pattern pattern, int i, int i1) { + throw new UnsupportedOperationException(); + } + + @Override + public int countMatches(ByteArray searchTerm) { + throw new UnsupportedOperationException(); + } + + @Override + public int countMatches(String searchTerm) { + throw new UnsupportedOperationException(); + } + + @Override + public int countMatches(ByteArray searchTerm, boolean caseSensitive) { + throw new UnsupportedOperationException(); + } + + @Override + public int countMatches(String searchTerm, boolean caseSensitive) { + throw new UnsupportedOperationException(); + } + + @Override + public int countMatches(ByteArray searchTerm, boolean caseSensitive, int startIndexInclusive, int endIndexExclusive) { + throw new UnsupportedOperationException(); + } + + @Override + public int countMatches(String searchTerm, boolean caseSensitive, int startIndexInclusive, int endIndexExclusive) { + throw new UnsupportedOperationException(); + } + + @Override + public int countMatches(Pattern pattern) { + throw new UnsupportedOperationException(); + } + + @Override + public int countMatches(Pattern pattern, int i, int i1) { + throw new UnsupportedOperationException(); + } + + @Override + public ByteArray withAppended(byte... data) { + throw new UnsupportedOperationException(); + } + + @Override + public ByteArray withAppended(int... data) { + throw new UnsupportedOperationException(); + } + + @Override + public ByteArray withAppended(String text) { + return withAppended(new FakeByteArray(text)); + } + + @Override + public ByteArray withAppended(ByteArray byteArray) { + byte[] temp = new byte[data.length + byteArray.length()]; + byte[] bytesToAppend = byteArray.getBytes(); + + System.arraycopy(data, 0, temp, 0, data.length); + System.arraycopy(bytesToAppend, 0, temp, data.length, bytesToAppend.length); + + return new FakeByteArray(temp); + } + + @Override + public Iterator iterator() { + throw new UnsupportedOperationException(); + } + + @Override + public String toString() { + return new String(data, UTF_8); + } +} \ No newline at end of file diff --git a/src/test/java/burp/api/montoya/core/FakeHttpHeader.java b/src/test/java/burp/api/montoya/core/FakeHttpHeader.java new file mode 100644 index 0000000..92501dd --- /dev/null +++ b/src/test/java/burp/api/montoya/core/FakeHttpHeader.java @@ -0,0 +1,24 @@ +package burp.api.montoya.core; + +import burp.api.montoya.http.message.HttpHeader; + +public class FakeHttpHeader implements HttpHeader { + + private final String name; + private final String value; + + public FakeHttpHeader(String name, String value) { + this.name = name; + this.value = value; + } + + @Override + public String name() { + return name; + } + + @Override + public String value() { + return value; + } +} diff --git a/src/test/java/burp/api/montoya/core/FakeHttpMessage.java b/src/test/java/burp/api/montoya/core/FakeHttpMessage.java new file mode 100644 index 0000000..3eee1e4 --- /dev/null +++ b/src/test/java/burp/api/montoya/core/FakeHttpMessage.java @@ -0,0 +1,162 @@ +package burp.api.montoya.core; + +import java.util.ArrayList; +import java.util.List; +import java.util.Scanner; +import java.util.regex.Pattern; + +import burp.api.montoya.http.message.HttpHeader; +import burp.api.montoya.http.message.HttpMessage; + +public class FakeHttpMessage implements HttpMessage { + + String rawContent; + + String header; + String body; + + List headerList = new ArrayList<>(); + + public FakeHttpMessage(String message) { + super(); + rawContent = message; + + splitHeaderAndBody(); + processHeader(); + } + + private void processHeader() { + List lines = List.of(header.split("\r\n")); + for (String line : lines) { + // body reached + if (line.isBlank()) { + return; + } + + int colon = line.indexOf(':'); + + if (colon > -1) { + headerList.add(HttpHeader.httpHeader(line.substring(0, colon), line.substring(colon + 1).trim())); + } + } + } + + public String toString() { + return rawContent; + } + + private void splitHeaderAndBody() { + Scanner scanner = new Scanner(this.rawContent); + String delimiter = "\r\n"; + scanner.useDelimiter(delimiter); + StringBuilder sb=new StringBuilder(); + List result=new ArrayList<>(); + while (scanner.hasNextLine()){ + String line = scanner.nextLine(); + if(!(line.trim().isEmpty())){ + sb.append(line).append(delimiter); + }else if(!sb.toString().isEmpty()) { + result.add(sb.toString()); + sb.setLength(0); + } + } + if(!sb.toString().isEmpty()) { + result.add(sb.toString()); + } + + header = result.get(0); + + if (result.size() > 1) { + body = result.get(1).trim(); // TODO + } else { + body = ""; + } + } + + @Override + public boolean hasHeader(HttpHeader header) { + return headerList.stream() + .anyMatch(o -> o.equals(header)); + } + + @Override + public boolean hasHeader(String name) { + return headerList.stream() + .anyMatch(o -> o.name().equals(name)); + } + + @Override + public boolean hasHeader(String name, String value) { + return headerList.stream() + .anyMatch(o -> o.name().equals(name) && o.value().equals(value)); + } + + @Override + public HttpHeader header(String name) { + return headerList.stream() + .filter(o -> o.name().equals(name)) + .findFirst() + .orElse(null); + } + + @Override + public String headerValue(String name) { + return headerList.stream() + .filter(o -> o.name().equals(name)) + .map(HttpHeader::value) + .findFirst() + .orElse(null); + } + + @Override + public List headers() { + return headerList; + } + + @Override + public String httpVersion() { + System.err.println("Not implemented"); + return ""; + } + + @Override + public int bodyOffset() { + System.err.println("Not implemented"); + return 0; + } + + @Override + public ByteArray body() { + System.err.println("Not implemented"); + return null; + } + + @Override + public String bodyToString() { + return body; + } + + @Override + public List markers() { + System.err.println("Not implemented"); + return List.of(); + } + + @Override + public boolean contains(String searchTerm, boolean caseSensitive) { + System.err.println("Not implemented"); + return false; + } + + @Override + public boolean contains(Pattern pattern) { + System.err.println("Not implemented"); + return false; + } + + @Override + public ByteArray toByteArray() { + System.err.println("Not implemented"); + return null; + } +} diff --git a/src/test/java/burp/api/montoya/core/FakeHttpParameter.java b/src/test/java/burp/api/montoya/core/FakeHttpParameter.java new file mode 100644 index 0000000..0d9beaa --- /dev/null +++ b/src/test/java/burp/api/montoya/core/FakeHttpParameter.java @@ -0,0 +1,41 @@ +package burp.api.montoya.core; + +import burp.api.montoya.http.message.params.HttpParameter; +import burp.api.montoya.http.message.params.HttpParameterType; + +public class FakeHttpParameter implements HttpParameter { + + private final String name; + private final String value; + private final HttpParameterType type; + + public FakeHttpParameter(String name, String value, HttpParameterType type) { + this.name = name; + this.value = value; + this.type = type; + } + + @Override + public String toString() { + return "FakeHttpParameter{" + "name='" + name + '\'' + ", value='" + value + '\'' + ", type=" + type + '}'; + } + + public String toNameValueString() { + return name + "=" + value; + } + + @Override + public HttpParameterType type() { + return type; + } + + @Override + public String name() { + return name; + } + + @Override + public String value() { + return value; + } +} \ No newline at end of file diff --git a/src/test/java/burp/api/montoya/core/FakeHttpRequest.java b/src/test/java/burp/api/montoya/core/FakeHttpRequest.java new file mode 100644 index 0000000..5c29cad --- /dev/null +++ b/src/test/java/burp/api/montoya/core/FakeHttpRequest.java @@ -0,0 +1,298 @@ +package burp.api.montoya.core; + +import burp.api.montoya.http.HttpService; +import burp.api.montoya.http.message.ContentType; +import burp.api.montoya.http.message.HttpHeader; +import burp.api.montoya.http.message.params.HttpParameter; +import burp.api.montoya.http.message.params.HttpParameterType; +import burp.api.montoya.http.message.params.ParsedHttpParameter; +import burp.api.montoya.http.message.requests.HttpRequest; +import burp.api.montoya.http.message.requests.HttpTransformation; + +import java.util.ArrayList; +import java.util.List; +import java.util.stream.Collectors; + +public class FakeHttpRequest extends FakeHttpMessage implements HttpRequest { + + List parameters = new ArrayList<>();; + + private FakeHttpRequest(FakeHttpRequest request, HttpParameter parameter) { + this(request.rawContent); + + // update parameters + parameters = parameters.stream() + .map(o -> o.name().equals(parameter.name()) ? new FakeParsedHttpParameter(parameter.name(), parameter.value(), parameter.type(), new FakeRange(0, 0), new FakeRange(0, 0)) : o) + .toList(); + + // update body + this.body = parameters.stream().filter(o -> o.type() == HttpParameterType.BODY).map(o -> ((FakeParsedHttpParameter) o).toNameValueString()).collect(Collectors.joining("&")); + + // update raw content + this.rawContent = this.header + "\r\n" + this.body; + } + + public FakeHttpRequest(String request) { + super(request); + + processBody(); + } + + private void processBody() { + List params = List.of(body.split("&")); + for (String param : params) { + String[] keyValue = param.split("="); + if (keyValue.length == 2) { + String name = keyValue[0].trim(); + String value = keyValue[1].trim(); + + // range is not yet supported + parameters.add(new FakeParsedHttpParameter(name, value, HttpParameterType.BODY, new FakeRange(0, 0), new FakeRange(0, 1))); + } + } + } + + @Override + public boolean isInScope() { + System.err.println("Not implemented"); + return false; + } + + @Override + public HttpService httpService() { + System.err.println("Not implemented"); + return null; + } + + @Override + public String url() { + System.err.println("Not implemented"); + return ""; + } + + @Override + public String method() { + System.err.println("Not implemented"); + return ""; + } + + @Override + public String path() { + System.err.println("Not implemented"); + return ""; + } + + @Override + public String query() { + System.err.println("Not implemented"); + return ""; + } + + @Override + public String pathWithoutQuery() { + System.err.println("Not implemented"); + return ""; + } + + @Override + public String fileExtension() { + System.err.println("Not implemented"); + return ""; + } + + @Override + public ContentType contentType() { + System.err.println("Not implemented"); + return null; + } + + @Override + public List parameters() { + return parameters; + } + + @Override + public List parameters(HttpParameterType type) { + return parameters.stream().filter(parameter -> (parameter.type() == type)).toList(); + } + + @Override + public boolean hasParameters() { + return !parameters.isEmpty(); + } + + @Override + public boolean hasParameters(HttpParameterType type) { + return !parameters.stream().filter(parameter -> (parameter.type() == type)).toList().isEmpty(); + } + + @Override + public ParsedHttpParameter parameter(String name, HttpParameterType type) { + return parameters.stream().filter(parameter -> (parameter.name().equals(name) && (parameter.type() == type))).findAny().orElse(null); + } + + @Override + public String parameterValue(String name, HttpParameterType type) { + return parameters.stream().filter(parameter -> (parameter.name().equals(name) && (parameter.type() == type))).findAny().map(ParsedHttpParameter::value).orElse(null); + } + + @Override + public boolean hasParameter(String name, HttpParameterType type) { + return parameters.stream().anyMatch(parameter -> (parameter.name().equals(name) && parameter.type() == type)); + } + + @Override + public boolean hasParameter(HttpParameter parameter) { + System.err.println("Not implemented"); + return false; + } + + @Override + public HttpRequest copyToTempFile() { + System.err.println("Not implemented"); + return null; + } + + @Override + public HttpRequest withService(HttpService service) { + System.err.println("Not implemented"); + return null; + } + + @Override + public HttpRequest withPath(String path) { + System.err.println("Not implemented"); + return null; + } + + @Override + public HttpRequest withMethod(String method) { + System.err.println("Not implemented"); + return null; + } + + @Override + public HttpRequest withHeader(HttpHeader header) { + System.err.println("Not implemented"); + return null; + } + + @Override + public HttpRequest withHeader(String name, String value) { + System.err.println("Not implemented"); + return null; + } + + @Override + public HttpRequest withParameter(HttpParameter parameter) { + return new FakeHttpRequest(this, parameter); + } + + @Override + public HttpRequest withAddedParameters(List parameters) { + System.err.println("Not implemented"); + return null; + } + + @Override + public HttpRequest withAddedParameters(HttpParameter... parameters) { + System.err.println("Not implemented"); + return null; + } + + @Override + public HttpRequest withRemovedParameters(List parameters) { + System.err.println("Not implemented"); + return null; + } + + @Override + public HttpRequest withRemovedParameters(HttpParameter... parameters) { + System.err.println("Not implemented"); + return null; + } + + @Override + public HttpRequest withUpdatedParameters(List parameters) { + System.err.println("Not implemented"); + return null; + } + + @Override + public HttpRequest withUpdatedParameters(HttpParameter... parameters) { + System.err.println("Not implemented"); + return null; + } + + @Override + public HttpRequest withTransformationApplied(HttpTransformation transformation) { + System.err.println("Not implemented"); + return null; + } + + @Override + public HttpRequest withBody(String body) { + System.err.println("Not implemented"); + return null; + } + + @Override + public HttpRequest withBody(ByteArray body) { + System.err.println("Not implemented"); + return null; + } + + @Override + public HttpRequest withAddedHeader(String name, String value) { + System.err.println("Not implemented"); + return null; + } + + @Override + public HttpRequest withAddedHeader(HttpHeader header) { + System.err.println("Not implemented"); + return null; + } + + @Override + public HttpRequest withUpdatedHeader(String name, String value) { + System.err.println("Not implemented"); + return null; + } + + @Override + public HttpRequest withUpdatedHeader(HttpHeader header) { + System.err.println("Not implemented"); + return null; + } + + @Override + public HttpRequest withRemovedHeader(String name) { + System.err.println("Not implemented"); + return null; + } + + @Override + public HttpRequest withRemovedHeader(HttpHeader header) { + System.err.println("Not implemented"); + return null; + } + + @Override + public HttpRequest withMarkers(List markers) { + System.err.println("Not implemented"); + return null; + } + + @Override + public HttpRequest withMarkers(Marker... markers) { + System.err.println("Not implemented"); + return null; + } + + @Override + public HttpRequest withDefaultHeaders() { + System.err.println("Not implemented"); + return null; + } +} diff --git a/src/test/java/burp/api/montoya/core/FakeHttpResponse.java b/src/test/java/burp/api/montoya/core/FakeHttpResponse.java new file mode 100644 index 0000000..e3b3cbc --- /dev/null +++ b/src/test/java/burp/api/montoya/core/FakeHttpResponse.java @@ -0,0 +1,154 @@ +package burp.api.montoya.core; + +import burp.api.montoya.http.message.Cookie; +import burp.api.montoya.http.message.HttpHeader; +import burp.api.montoya.http.message.MimeType; +import burp.api.montoya.http.message.StatusCodeClass; +import burp.api.montoya.http.message.responses.HttpResponse; +import burp.api.montoya.http.message.responses.analysis.Attribute; +import burp.api.montoya.http.message.responses.analysis.AttributeType; +import burp.api.montoya.http.message.responses.analysis.KeywordCount; + +import java.util.List; + +public class FakeHttpResponse extends FakeHttpMessage implements HttpResponse { + + public FakeHttpResponse(String message) { + super(message); + } + + @Override + public short statusCode() { + return 0; + } + + @Override + public String reasonPhrase() { + return ""; + } + + @Override + public boolean isStatusCodeClass(StatusCodeClass statusCodeClass) { + return false; + } + + @Override + public List cookies() { + return List.of(); + } + + @Override + public Cookie cookie(String name) { + return null; + } + + @Override + public String cookieValue(String name) { + return ""; + } + + @Override + public boolean hasCookie(String name) { + return false; + } + + @Override + public boolean hasCookie(Cookie cookie) { + return false; + } + + @Override + public MimeType mimeType() { + return null; + } + + @Override + public MimeType statedMimeType() { + return null; + } + + @Override + public MimeType inferredMimeType() { + return null; + } + + @Override + public List keywordCounts(String... keywords) { + return List.of(); + } + + @Override + public List attributes(AttributeType... types) { + return List.of(); + } + + @Override + public HttpResponse copyToTempFile() { + return null; + } + + @Override + public HttpResponse withStatusCode(short statusCode) { + return null; + } + + @Override + public HttpResponse withReasonPhrase(String reasonPhrase) { + return null; + } + + @Override + public HttpResponse withHttpVersion(String httpVersion) { + return null; + } + + @Override + public HttpResponse withBody(String body) { + return null; + } + + @Override + public HttpResponse withBody(ByteArray body) { + return null; + } + + @Override + public HttpResponse withAddedHeader(HttpHeader header) { + return null; + } + + @Override + public HttpResponse withAddedHeader(String name, String value) { + return null; + } + + @Override + public HttpResponse withUpdatedHeader(HttpHeader header) { + return null; + } + + @Override + public HttpResponse withUpdatedHeader(String name, String value) { + return null; + } + + @Override + public HttpResponse withRemovedHeader(HttpHeader header) { + return null; + } + + @Override + public HttpResponse withRemovedHeader(String name) { + return null; + } + + @Override + public HttpResponse withMarkers(List markers) { + return null; + } + + @Override + public HttpResponse withMarkers(Marker... markers) { + return null; + } +} diff --git a/src/test/java/burp/api/montoya/core/FakeParsedHttpParameter.java b/src/test/java/burp/api/montoya/core/FakeParsedHttpParameter.java new file mode 100644 index 0000000..69f35cc --- /dev/null +++ b/src/test/java/burp/api/montoya/core/FakeParsedHttpParameter.java @@ -0,0 +1,27 @@ +package burp.api.montoya.core; + +import burp.api.montoya.http.message.params.HttpParameterType; +import burp.api.montoya.http.message.params.ParsedHttpParameter; + +public class FakeParsedHttpParameter extends FakeHttpParameter implements ParsedHttpParameter { + + private final Range nameOffset; + private final Range valueOffset; + + public FakeParsedHttpParameter(String name, String value, HttpParameterType type, Range nameOffset, Range valueOffset) { + super(name, value, type); + + this.nameOffset = nameOffset; + this.valueOffset = valueOffset; + } + + @Override + public Range nameOffsets() { + return nameOffset; + } + + @Override + public Range valueOffsets() { + return valueOffset; + } +} diff --git a/src/test/java/burp/api/montoya/core/FakeRange.java b/src/test/java/burp/api/montoya/core/FakeRange.java new file mode 100644 index 0000000..196c3ea --- /dev/null +++ b/src/test/java/burp/api/montoya/core/FakeRange.java @@ -0,0 +1,53 @@ +// copied from https://github.com/Hannah-PortSwigger/WebSocketTurboIntruder/blob/main/src/test/java/burp/api/montoya/core/FakeRange.java + +package burp.api.montoya.core; + +import java.util.Objects; + +public class FakeRange implements Range { + private final int start; + private final int end; + + public FakeRange(int start, int end) { + this.start = start; + this.end = end; + } + + @Override + public int startIndexInclusive() { + return start; + } + + @Override + public int endIndexExclusive() { + return end; + } + + @Override + public boolean contains(int i) { + throw new UnsupportedOperationException(); + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + + return o instanceof Range range && startIndexInclusive() == range.startIndexInclusive() && endIndexExclusive() == range.endIndexExclusive(); + } + + @Override + public int hashCode() { + return Objects.hash(start, end); + } + + @Override + public String toString() { + return "Range{" + "start=" + start + ", end=" + end + '}'; + } + + public static Range rangeOf(int start, int end) { + return new FakeRange(start, end); + } +} \ No newline at end of file diff --git a/test/app/TestAuthorizationDetection.java b/test/app/TestAuthorizationDetection.java deleted file mode 100644 index 7c4da71..0000000 --- a/test/app/TestAuthorizationDetection.java +++ /dev/null @@ -1,51 +0,0 @@ -package app; - -import static org.assertj.core.api.Assertions.assertThat; - -import static java.util.Arrays.asList; - -import static app.TestTokens.HS256_TOKEN; -import static app.TestTokens.INVALID_TOKEN; - -import java.util.List; - -import org.junit.jupiter.api.Test; - -import app.tokenposition.AuthorizationBearerHeader; - -class TestAuthorizationDetection { - - @Test - void testAuthValid() { - List headers = asList("GET /jwt/response_cookie.php HTTP/1.1", - "Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8", "Accept-Language: en-US,en;q=0.5", - "Authorization: Bearer " + HS256_TOKEN, "Connection: close", "Upgrade-Insecure-Requests: 1"); - - AuthorizationBearerHeader abh = new AuthorizationBearerHeader(headers, ""); - - assertThat(abh.positionFound()).isTrue(); - assertThat(abh.getToken()).isEqualTo(HS256_TOKEN); - } - - @Test - void testAuthInvalid() { - List headers = asList("GET /jwt/response_cookie.php HTTP/1.1", - "Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8", "Accept-Language: en-US,en;q=0.5", - "Authorization: Bearer " + INVALID_TOKEN, "Connection: close", "Upgrade-Insecure-Requests: 1"); - - AuthorizationBearerHeader abh = new AuthorizationBearerHeader(headers, ""); - - assertThat(abh.positionFound()).isFalse(); - } - - @Test - void testAuthInvalid2() { - List headers = asList("GET /jwt/response_cookie.php HTTP/1.1", - "Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8", "Accept-Language: en-US,en;q=0.5", - "Authorization: Bearer topsecret123456789!", "Connection: close", "Upgrade-Insecure-Requests: 1"); - - AuthorizationBearerHeader abh = new AuthorizationBearerHeader(headers, ""); - - assertThat(abh.positionFound()).isFalse(); - } -} diff --git a/test/app/TestBodyDetection.java b/test/app/TestBodyDetection.java deleted file mode 100644 index 03cbbb3..0000000 --- a/test/app/TestBodyDetection.java +++ /dev/null @@ -1,90 +0,0 @@ -package app; - -import app.helpers.KeyValuePair; -import app.tokenposition.Body; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.params.ParameterizedTest; -import org.junit.jupiter.params.provider.Arguments; -import org.junit.jupiter.params.provider.MethodSource; - -import java.util.stream.Stream; - -import static app.TestTokens.*; -import static org.assertj.core.api.Assertions.assertThat; -import static org.junit.jupiter.params.provider.Arguments.arguments; - -class TestBodyDetection { - - static Stream bodyDataAndDetectedTokens() { - return Stream.of( - arguments("test=best&abc=" + HS256_TOKEN, HS256_TOKEN), - arguments("a " + HS256_TOKEN + " b", HS256_TOKEN), - arguments("{\"aaaa\":\"" + HS256_TOKEN + "\"}", HS256_TOKEN), - arguments("{ \"bbbb\" : { \" cccc \": { \" dddd\":\"" + HS256_TOKEN + " \"}}}", HS256_TOKEN), - arguments("token=" + HS256_TOKEN, HS256_TOKEN), - arguments(HS256_TOKEN, HS256_TOKEN), - arguments("egg=" + HS256_TOKEN + "&test=best", HS256_TOKEN), - arguments("abc def " + HS256_TOKEN + " ghi jkl", HS256_TOKEN) - ); - } - - @MethodSource("bodyDataAndDetectedTokens") - @ParameterizedTest - void testBodyDetection(String body, String bodyToken) { - Body pb = new Body(null, body); - - KeyValuePair result = pb.getJWTFromBody(); - - assertThat(result.getValue()).isEqualTo(bodyToken); - } - - static Stream bodyDataWhereNoTokenDetected() { - return Stream.of( - arguments("{ \"bbbb\" : { \" cccc \": { \" dddd\":" + HS256_TOKEN + " \"}}}"), - arguments("{}"), - arguments("{ \"abc\": \"def\"}"), - arguments("{ \"abc\": {\"def\" : \"ghi\"} }"), - arguments("yo=" + INVALID_TOKEN + "&test=best"), - arguments("ab " + INVALID_TOKEN + " de"), - arguments(HS256_TOKEN + "&"), - arguments("abc def ghi jkl"), - arguments("") - ); - } - - @MethodSource("bodyDataWhereNoTokenDetected") - @ParameterizedTest - void testBodyDetection(String body) { - Body pb = new Body(null, body); - - KeyValuePair result = pb.getJWTFromBody(); - - assertThat(result).isNull(); - } - - @Test - void testPostBodyReplaceWithParam() { - String body = "test=best&token=" + HS256_TOKEN; - Body pb = new Body(null, body); - @SuppressWarnings("unused") - KeyValuePair result = pb.getJWTFromBody(); - pb.getToken(); - String replaced = pb.replaceTokenImpl(HS256_TOKEN_2, body); - Body pbR = new Body(null, replaced); - KeyValuePair resultR = pbR.getJWTFromBody(); - assertThat(resultR.getValue()).isEqualTo(HS256_TOKEN_2); - } - - @Test - void testPostBodyReplaceWithoutParam() { - String body = "ab\n cd " + HS256_TOKEN + " cd"; - Body pb = new Body(null, body); - @SuppressWarnings("unused") - KeyValuePair result = pb.getJWTFromBody(); - pb.getToken(); - String replaced = pb.replaceTokenImpl(HS256_TOKEN_2, body); - Body pbR = new Body(null, replaced); - KeyValuePair resultR = pbR.getJWTFromBody(); - assertThat(resultR.getValue()).isEqualTo(HS256_TOKEN_2); - } -} diff --git a/test/app/TestCustomJWTDecoder.java b/test/app/TestCustomJWTDecoder.java deleted file mode 100644 index 0b3191d..0000000 --- a/test/app/TestCustomJWTDecoder.java +++ /dev/null @@ -1,35 +0,0 @@ -package app; - -import model.CustomJWToken; -import org.junit.jupiter.api.Test; - -import static app.TestTokens.*; -import static org.assertj.core.api.Assertions.assertThat; - -class TestCustomJWTDecoder { - - @Test - void testIfTokenCanBeDecoded() { - CustomJWToken reConstructedToken = new CustomJWToken(HS256_TOKEN); - assertThat(HS256_TOKEN).isEqualTo(reConstructedToken.getToken()); - assertThat(reConstructedToken.isBuiltSuccessful()).isTrue(); - } - - @Test - void testBrokenToken() { - CustomJWToken reConstructedToken = new CustomJWToken(INVALID_TOKEN); - assertThat(reConstructedToken.isBuiltSuccessful()).isFalse(); - } - - @Test - void testIfTokenIsMinified() { - CustomJWToken reConstructedToken = new CustomJWToken(HS256_TOKEN); - assertThat(reConstructedToken.isMinified()).isTrue(); - } - - @Test - void testIfTokenIsNotMinified() { - CustomJWToken reConstructedToken = new CustomJWToken(HS256_BEAUTIFIED_TOKEN); - assertThat(reConstructedToken.isMinified()).isFalse(); - } -} \ No newline at end of file diff --git a/test/app/TestPostDetection.java b/test/app/TestPostDetection.java deleted file mode 100644 index 5970c16..0000000 --- a/test/app/TestPostDetection.java +++ /dev/null @@ -1,65 +0,0 @@ -package app; - - -import app.helpers.KeyValuePair; -import app.tokenposition.PostBody; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.params.ParameterizedTest; -import org.junit.jupiter.params.provider.Arguments; -import org.junit.jupiter.params.provider.MethodSource; - -import java.util.stream.Stream; - -import static app.TestTokens.*; -import static org.assertj.core.api.Assertions.assertThat; -import static org.junit.jupiter.params.provider.Arguments.arguments; - -class TestPostDetection { - - static Stream postDataAndDetectedTokens() { - return Stream.of( - arguments("test=best&token=" + HS256_TOKEN, HS256_TOKEN), - arguments("token=" + HS256_TOKEN, HS256_TOKEN), - arguments("token=" + HS256_TOKEN + "&test=best", HS256_TOKEN) - ); - } - - @MethodSource("postDataAndDetectedTokens") - @ParameterizedTest - void testPostBody(String body, String bodyToken) { - PostBody pb = new PostBody(null, body); - - KeyValuePair result = pb.getJWTFromPostBody(); - - assertThat(result.getValue()).isEqualTo(bodyToken); - } - - static Stream postDataWhereNoTokenDetected() { - return Stream.of( - arguments("token=" + INVALID_TOKEN + "&test=best"), - arguments("") - ); - } - - @MethodSource("postDataWhereNoTokenDetected") - @ParameterizedTest - void testPostBody(String body) { - PostBody pb = new PostBody(null, body); - - KeyValuePair result = pb.getJWTFromPostBody(); - - assertThat(result).isNull(); - } - - @Test - void testPostBodyReplace() { - String body = "test=best&token=" + HS256_TOKEN; - PostBody pb = new PostBody(null, body); - - String replaced = pb.replaceTokenImpl(HS256_TOKEN_2, body); - PostBody pbR = new PostBody(null, replaced); - KeyValuePair resultR = pbR.getJWTFromPostBody(); - - assertThat(resultR.getValue()).isEqualTo(HS256_TOKEN_2); - } -} diff --git a/test/app/TestSetCookieDetection.java b/test/app/TestSetCookieDetection.java deleted file mode 100644 index 144ba33..0000000 --- a/test/app/TestSetCookieDetection.java +++ /dev/null @@ -1,227 +0,0 @@ -package app; - -import app.tokenposition.Cookie; -import org.junit.jupiter.api.Test; - -import java.util.ArrayList; -import java.util.List; - -import static app.TestTokens.*; -import static java.util.Arrays.asList; -import static org.assertj.core.api.Assertions.assertThat; - -class TestSetCookieDetection { - - @Test - void testCookieReversedOrder() { - List headers = asList( - "GET /jwt/response_cookie.php HTTP/1.1", - "Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8", - "Accept-Language: en-US,en;q=0.5", - "Set-Cookie: token=" + HS256_TOKEN, - "Set-Cookie: test=best", - "Connection: close", - "Upgrade-Insecure-Requests: 1" - ); - Cookie cookie = new Cookie(headers, ""); - String result = cookie.findJWTInHeaders(headers); - assertThat(result).isEqualTo(HS256_TOKEN); - } - - @Test - void testCookieInvalidJWT() { - List headers = asList( - "GET /jwt/response_cookie.php HTTP/1.1", - "Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8", - "Accept-Language: en-US,en;q=0.5", - "Set-Cookie: token=" + INVALID_TOKEN, - "Connection: close", - "Upgrade-Insecure-Requests: 1" - ); - Cookie cookie = new Cookie(headers, ""); - String result = cookie.findJWTInHeaders(headers); - assertThat(result).isNull(); - } - - @Test - void testCookieNoJWT() { - List headers = asList( - "GET /jwt/response_cookie.php HTTP/1.1", - "Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8", - "Accept-Language: en-US,en;q=0.5", - "Cookie: test=besst", - "Connection: close", - "Upgrade-Insecure-Requests: 1" - ); - Cookie cookie = new Cookie(headers, ""); - String result = cookie.findJWTInHeaders(headers); - assertThat(result).isNull(); - } - - @Test - void testSetCookieGetTokenInvalid() { - List headers = asList( - "GET /jwt/response_cookie.php HTTP/1.1", - "Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8", - "Accept-Language: en-US,en;q=0.5", - "Set-Cookie: token=" + INVALID_TOKEN, - "Connection: close", - "Upgrade-Insecure-Requests: 1" - ); - Cookie cookie = new Cookie(headers, ""); - String result = cookie.findJWTInHeaders(headers); - assertThat(result).isNull(); - assertThat(cookie.getToken()).isEmpty(); - assertThat(cookie.positionFound()).isFalse(); - } - - @Test - void testSetCookieInvalid() { - List headers = asList( - "GET /jwt/response_cookie.php HTTP/1.1", - "Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8", - "Accept-Language: en-US,en;q=0.5", - "Set-Cookie: token=" + INVALID_TOKEN, - "Connection: close", - "Upgrade-Insecure-Requests: 1" - ); - Cookie cookie = new Cookie(headers, ""); - String result = cookie.findJWTInHeaders(headers); - assertThat(result).isNull(); - } - - @Test - void testSetCookieSemicolon() { - List headers = asList( - "GET /jwt/response_cookie.php HTTP/1.1", - "Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8", - "Accept-Language: en-US,en;q=0.5", - "Set-Cookie: token=" + HS256_TOKEN + ";", - "Connection: close", - "Upgrade-Insecure-Requests: 1" - ); - Cookie cookie = new Cookie(headers, ""); - String result = cookie.findJWTInHeaders(headers); - assertThat(result).isEqualTo(HS256_TOKEN); - } - - @Test - void testSetCookieReplace() { - List headers = new ArrayList<>( - asList( - "GET /jwt/response_cookie.php HTTP/1.1", - "Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8", - "Accept-Language: en-US,en;q=0.5", - "Set-Cookie: token=" + HS256_TOKEN + ";", - "Connection: close", - "Upgrade-Insecure-Requests: 1" - ) - ); - Cookie cookie = new Cookie(headers, ""); - String result = cookie.findJWTInHeaders(headers); - assertThat(result).isEqualTo(HS256_TOKEN); - List replaces = cookie.replaceTokenInHeader(HS256_TOKEN_2, headers); - Cookie cookieR = new Cookie(headers, ""); - String resultR = cookieR.findJWTInHeaders(replaces); - - assertThat(resultR).isEqualTo(HS256_TOKEN_2); - } - - @Test - void testSetCookie() { - List headers = asList( - "GET /jwt/response_cookie.php HTTP/1.1", - "Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8", - "Accept-Language: en-US,en;q=0.5", - "Set-Cookie: token=" + HS256_TOKEN, - "Connection: close", - "Upgrade-Insecure-Requests: 1" - ); - Cookie cookie = new Cookie(headers, ""); - String result = cookie.findJWTInHeaders(headers); - assertThat(result).isEqualTo(HS256_TOKEN); - } - - @Test - void testSetCookieGetToken() { - List headers = asList( - "GET /jwt/response_cookie.php HTTP/1.1", - "Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8", - "Accept-Language: en-US,en;q=0.5", - "Set-Cookie: token=" + HS256_TOKEN, - "Connection: close", - "Upgrade-Insecure-Requests: 1" - ); - Cookie cookie = new Cookie(headers, ""); - @SuppressWarnings("unused") - String result = cookie.findJWTInHeaders(headers); - assertThat(cookie.getToken()).isEqualTo(HS256_TOKEN); - } - - @Test - void testSetCookieSecureFlag() { - List headers = asList( - "GET /jwt/response_cookie.php HTTP/1.1", - "Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8", - "Accept-Language: en-US,en;q=0.5", - "Set-Cookie: token=" + HS256_TOKEN + "; expires=Thu, 01-Jan-1970 01:40:00 GMT; Max-Age=0; path=/; secure;", - "Connection: close", - "Upgrade-Insecure-Requests: 1" - ); - Cookie cookie = new Cookie(headers, ""); - String result = cookie.findJWTInHeaders(headers); - assertThat(result).isEqualTo(HS256_TOKEN); - assertThat(cookie.getcFW().hasSecureFlag()).isTrue(); - } - - @Test - void testSetCookieHTTPOnlyFlag() { - List headers = asList( - "GET /jwt/response_cookie.php HTTP/1.1", - "Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8", - "Accept-Language: en-US,en;q=0.5", - "Set-Cookie: token=" + HS256_TOKEN + "; expires=Thu, 01-Jan-1970 01:40:00 GMT; HttpOnly; Max-Age=0; path=/;", - "Connection: close", - "Upgrade-Insecure-Requests: 1" - ); - Cookie cookie = new Cookie(headers, ""); - String result = cookie.findJWTInHeaders(headers); - assertThat(result).isEqualTo(HS256_TOKEN); - assertThat(cookie.getcFW().hasHttpOnlyFlag()).isTrue(); - } - - @Test - void testSetCookieBothFlags() { - List headers = asList( - "GET /jwt/response_cookie.php HTTP/1.1", - "Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8", - "Accept-Language: en-US,en;q=0.5", - "Set-Cookie: token=" + HS256_TOKEN + "; expires=Thu, 01-Jan-1970 01:40:00 GMT; HttpOnly; Max-Age=0; secure; path=/;", - "Connection: close", - "Upgrade-Insecure-Requests: 1" - ); - Cookie cookie = new Cookie(headers, ""); - String result = cookie.findJWTInHeaders(headers); - assertThat(result).isEqualTo(HS256_TOKEN); - assertThat(cookie.getcFW().hasHttpOnlyFlag()).isTrue(); - assertThat(cookie.getcFW().hasSecureFlag()).isTrue(); - } - - @Test - void testSetCookieNoFlags() { - List headers = asList( - "GET /jwt/response_cookie.php HTTP/1.1", - "Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8", - "Accept-Language: en-US,en;q=0.5", - "Set-Cookie: token=" + HS256_TOKEN + "; expires=Thu, 01-Jan-1970 01:40:00 GMT; Max-Age=0; path=/;", - "Connection: close", - "Upgrade-Insecure-Requests: 1" - ); - Cookie cookie = new Cookie(headers, ""); - String result = cookie.findJWTInHeaders(headers); - - assertThat(result).isEqualTo(HS256_TOKEN); - assertThat(cookie.getcFW().hasHttpOnlyFlag()).isFalse(); - assertThat(cookie.getcFW().hasSecureFlag()).isFalse(); - } -} diff --git a/test/app/TestTokens.java b/test/app/TestTokens.java deleted file mode 100644 index 0b6f30a..0000000 --- a/test/app/TestTokens.java +++ /dev/null @@ -1,22 +0,0 @@ -package app; - -interface TestTokens { - String HS256_TOKEN = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWV9.TJVA95OrM7E2cBab30RMHrHDcEfxjoYZgeFONFh7HgQ"; - String HS256_BEAUTIFIED_TOKEN = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.ewogICAic3ViIjoiMTIzNDU2Nzg5MCIsCiAgICJuYW1lIjoiSm9obiBEb2UiLAogICAiYWRtaW4iOnRydWUKfQ==.TJVA95OrM7E2cBab30RMHrHDcEfxjoYZgeFONFh7HgQ"; - String HS256_TOKEN_2 = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6Ik1heCBNdXN0ZXJsaSIsImFkbWluIjp0cnVlfQ.9o7iXB3CEm8ciIJjc_yZPI49p7gSKX6zDddr3Gp5_hU"; - String INVALID_TOKEN = "eyJhbFbiOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWV9.TJVA95OrM7E2cBab30RMHrHDcEfxjYZgeFONFh7HgQ"; - String INVALID_TOKEN_2 = "eyJhbFb___RANDOM_GARBAGE___ZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWV9.TJVA95OrM7E2cBab30RMHrHDcEfxjYZgeFONFh7HgQ"; - - String ES256_TOKEN = "eyJhbGciOiJFUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWUsImlhdCI6MTUxNjIzOTAyMn0.tyh-VfuzIxCyGYDlkBA7DfyjrqmSHu6pQ2hoZuFqUSLPNY2N0mpHb3nk5K17HWP_3cYHBw7AhHale5wky6-sVA"; - String ES256_TOKEN_PUB = "-----BEGIN PUBLIC KEY-----MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEEVs/o5+uQbTjL3chynL4wXgUg2R9q9UU8I5mEovUf86QZ7kOBIjJwqnzD1omageEHWwHdBO6B+dFabmdT9POxg==-----END PUBLIC KEY-----"; - - // How To - CLI - // echo -n '{"alg":"RS256","typ":"JWT"}' | base64 | sed s/\+/-/ | sed -E s/=+$// - // echo -n '{"sub":"RS256inOTA","name":"John Doe"}' | base64 | sed s/\+/-/ | sed -E s/=+$// - // openssl genrsa 2048 > jwtRSA256-private.pem - // openssl rsa -in jwtRSA256-private.pem -pubout -outform PEM -out jwtRSA256-public.pem - // echo -n "eyJhbGciOiJFUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJFUzI1NmluT1RBIiwibmFtZSI6IkpvaG4gRG9lIn0" | openssl dgst -sha256 -binary -sign jwtRSA256-private.pem | openssl enc -base64 | tr -d '\n=' | tr -- '+/' '-_' - String RS256_TOKEN = "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJSUzI1NmluT1RBIiwibmFtZSI6IkpvaG4gRG9lIn0.VnF6UI5CHgOcg4T-k04xWLy5DW_-BiH75ccS9EpF1KP-5QAPKSqhls558cSa2DBPj5yeoFql9DFZ9H_mthbtz_HSfQ1DEDviP5mVfx9c5scEE9ebCaz9a5fQ_2uS2urh6HFTV7kGzjRqKJOCmB6gqtgGsPioDtrWU4o9mlqCh7k3meKTk5AJjeULgts96H2or4P9SUPXmI4Bv97bfSoj8LD3aHgI5FeKBU1KBEDFgDwy3WSI-SBlkf-43EQZwMgIvSVgqY9VXkJnS2aeu76oRn1MzpJBxWVRQaBrTZRnB0CCt3JjtK1QtIGHkl-M9-bVviQ-XtVqp52-DPG2GZFpqQ"; - String RS256_TOKEN_PUB = "-----BEGIN PUBLIC KEY-----MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAss7FTpt5OpOsNbb5bfmLZnn0D7NzkxqWn4s2r3ZkPcDFMLF4/31sJHCdNkiavFaM7w+DfuSXb0rSQ1Eh/WX9UPR/BN0a8BRzogfzcXOekt4DdnLZibkYtcBfg519tbNVu6geuYi4QbwXrtJUfEAGSbvC3F11aO/qtPHJiwC5XHLgA8kteVXNgto6IBmq2bio9kKMVtceNjxGm6PnH9jBWB3cnlHYipg6hZlqfkiw8sF7UosfTqGn4ibTNUxNVNQw3K5w9S9YylaNq5HOVeHX1egz0aokkXoNwjV/31kG+SQq7MKiJ/PlCPbzY5e3++chEAg6dMKI/FOmIJIwbw1rHwIDAQAB-----END PUBLIC KEY-----"; - String RS256_TOKEN_PRIV = "-----BEGIN RSA PRIVATE KEY-----MIIEpAIBAAKCAQEAss7FTpt5OpOsNbb5bfmLZnn0D7NzkxqWn4s2r3ZkPcDFMLF4/31sJHCdNkiavFaM7w+DfuSXb0rSQ1Eh/WX9UPR/BN0a8BRzogfzcXOekt4DdnLZibkYtcBfg519tbNVu6geuYi4QbwXrtJUfEAGSbvC3F11aO/qtPHJiwC5XHLgA8kteVXNgto6IBmq2bio9kKMVtceNjxGm6PnH9jBWB3cnlHYipg6hZlqfkiw8sF7UosfTqGn4ibTNUxNVNQw3K5w9S9YylaNq5HOVeHX1egz0aokkXoNwjV/31kG+SQq7MKiJ/PlCPbzY5e3++chEAg6dMKI/FOmIJIwbw1rHwIDAQABAoIBAGBQ7QtoyCZ7gVn10+ofb62lp4gFnA3zVoteS/i8B0cUXaPbFVhaUTRXzPd+qIsm/AeSDbz+mWwDm7tTKsH6fDdtXDZce7Qy8A6pxcKpCxQFr0vQlcmQAPV2SHz3Cs4jad0JtHMwaEBQd1leRtAfFMQG9fIKDcKW6ZDKZUwQ+cgH2XRFbEYtEgrw/G5+ZCwk7lENJCRVIqOGZH5ZmSrIgeEJP1sZgt1+qMDzDSiVV0EHdH07n/5SNuPawazSCa8/NOcpSyADdD3mJ6NN1icS6NAYeFS2mEecx5Wh3Dsx01E1YpdcF2m+nvuWqIlIl8DX1P9cs8fg1qJbedUXE21hg1ECgYEA7AJug9W1Xqt/fFhHCNGaILznsGCKS5FHzm6zOOHX1cH9zjXLl0Ywvv2ioX0utlOk5KxPUwxKTUvg1cvZ0WsJWckgcfkvKxZ/mfT3E56Ocf8i/Ra1R7dF0V+quXE0uNHvfQGyGqU8d6BkDXE3TFFkxzdcoBx1uvk1K8HOxsNtSBsCgYEAwfP9eWOguLFN9C/KuUbeGsRNO1rfQcPL2F8T7W5NaBKZl5f85KuG7JaSA7s4aIpSuACTGYE6AS/4AiShadjf+HXqXcTxBUWJyFoa+Py7Qfb1a4sPOQUnz/CXukiBSHXWmNqVbuKODu6ARmFoHUd0KvK4fPilOnmCfgVbjMtd4U0CgYEA6ltHztYKMiXuhFVMxG8Os++hyj0zVvK+8Thv884f+32VQI2ey2rBwQYv1lhuaFMK7KBGbNtJdRQiAWtZsmCtemEEPOkKc6j1sLXWG79ZB84oulUwUjSludFbwKWvis+9Fs72QwtNziSQ9eA03y37+u74pW1dYvtQV1EuuaUaAX0CgYEAwBFjPkbO7peG3v5E/12SrWcgJFtFI9dFkqv1C/djaGCjAWBd7AWAw+IIDvHkVoJEkDrhcSxryKk8LMMhpbRDd8UtplZVaCcI3wN8Gn4M4rIxL6KyHIFif6V+W9dZT+yB6zTrLrfkfhzposjrVbNg8vcSg4+n8FRMSYf8tVzfRzECgYAcsA/jdZthHEN6P0FycbAL4ALcCK1AcBVwdyrPjOm3sJ+7j0AoIRT9UlIyZ8xhtC3EX2iURJKlENdAnPQYThR+kCWGJq4CHhod9RhJgXzDyYYxyGcLBKTBcXjzZpx0jSguk3UobMdXgL2kG70tfwt1Y1b201OJmOLTg8sFmTYJRA==-----END RSA PRIVATE KEY-----"; -}