├── .gitignore ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── .editorconfig ├── LICENSE ├── test-helpers.sh ├── .github └── workflows │ └── gradle.yml ├── src ├── test │ └── java │ │ └── com │ │ └── github │ │ └── masonm │ │ ├── TestAuthHeader.java │ │ ├── JwtTest.java │ │ ├── JwtMatcherExtensionTest.java │ │ └── JwtStubMappingTransformerTest.java └── main │ └── java │ └── com │ └── github │ └── masonm │ ├── JwtExtensionStandalone.java │ ├── Jwt.java │ ├── JwtMatcherExtension.java │ └── JwtStubMappingTransformer.java ├── wiremock-jwt-extension.iml ├── test-request-match.sh ├── test-stub-transformer.sh ├── gradlew.bat ├── README.md └── gradlew /.gitignore: -------------------------------------------------------------------------------- 1 | /.gradle/ 2 | /.idea/ 3 | /build/ 4 | private.key 5 | /mappings/ 6 | /__files/ 7 | /docker/ 8 | /out/ 9 | -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MasonM/wiremock-jwt-extension/HEAD/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # EditorConfig is awesome: https://EditorConfig.org 2 | 3 | root = true 4 | 5 | [*] 6 | end_of_line = lf 7 | indent_style = space 8 | indent_size = 4 9 | 10 | [{*.yml,*.iml,gradlew}] 11 | indent_size = 2 12 | -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | distributionBase=GRADLE_USER_HOME 2 | distributionPath=wrapper/dists 3 | distributionUrl=https\://services.gradle.org/distributions/gradle-8.0.1-bin.zip 4 | networkTimeout=10000 5 | validateDistributionUrl=true 6 | zipStoreBase=GRADLE_USER_HOME 7 | zipStorePath=wrapper/dists 8 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright 2017 Mason Malone 2 | 3 | Licensed under the Apache License, Version 2.0 (the "License"); 4 | you may not use this file except in compliance with the License. 5 | You may obtain a copy of the License at 6 | 7 | http://www.apache.org/licenses/LICENSE-2.0 8 | 9 | Unless required by applicable law or agreed to in writing, software 10 | distributed under the License is distributed on an "AS IS" BASIS, 11 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | See the License for the specific language governing permissions and 13 | limitations under the License. 14 | 15 | -------------------------------------------------------------------------------- /test-helpers.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | VERSION=1.0.0 4 | 5 | launchWiremock() { 6 | echo "Launching Wiremock and setting up proxying" 7 | #java -cp wiremock-standalone-3.0.4.jar:build/libs/wiremock-jwt-extension-${VERSION}.jar wiremock.Run --extensions="com.github.masonm.JwtMatcherExtension,com.github.masonm.JwtStubMappingTransformer" & 8 | java -jar build/libs/wiremock-jwt-extension-${VERSION}-standalone.jar & 9 | WIREMOCK_PID=$! 10 | trap "kill $WIREMOCK_PID" exit 11 | 12 | echo -n "Waiting for Wiremock to start up." 13 | until $(curl --output /dev/null --silent --head ${WIREMOCK_BASE_URL}); do 14 | echo -n '.' 15 | sleep 1 16 | done 17 | } 18 | -------------------------------------------------------------------------------- /.github/workflows/gradle.yml: -------------------------------------------------------------------------------- 1 | # This workflow will build a Java project with Gradle 2 | # For more information see: https://help.github.com/actions/language-and-framework-guides/building-and-testing-java-with-gradle 3 | 4 | name: Java CI with Gradle 5 | 6 | on: 7 | push: 8 | branches: [ master ] 9 | pull_request: 10 | branches: [ master ] 11 | 12 | jobs: 13 | build: 14 | 15 | runs-on: ubuntu-latest 16 | 17 | steps: 18 | - uses: actions/checkout@v3 19 | - name: Set up Java 20 | uses: actions/setup-java@v3 21 | with: 22 | java-version: '11' 23 | distribution: 'temurin' 24 | - name: Build with Gradle 25 | run: ./gradlew build 26 | -------------------------------------------------------------------------------- /src/test/java/com/github/masonm/TestAuthHeader.java: -------------------------------------------------------------------------------- 1 | package com.github.masonm; 2 | 3 | import org.apache.commons.codec.binary.Base64; 4 | 5 | /** 6 | * Generates an Authorization header string for testing purposes 7 | */ 8 | public class TestAuthHeader { 9 | private final String header; 10 | private final String payload; 11 | 12 | public TestAuthHeader(String header, String payload) { 13 | this.header = header; 14 | this.payload = payload; 15 | } 16 | 17 | private String encode(String value) { 18 | return Base64.encodeBase64URLSafeString(value.getBytes()); 19 | } 20 | 21 | public String toString() { 22 | return "Bearer " + encode(header) + "." + encode(payload) + ".dummy_signature"; 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/main/java/com/github/masonm/JwtExtensionStandalone.java: -------------------------------------------------------------------------------- 1 | package com.github.masonm; 2 | 3 | import com.github.tomakehurst.wiremock.standalone.WireMockServerRunner; 4 | 5 | import java.util.Arrays; 6 | 7 | public final class JwtExtensionStandalone { 8 | private JwtExtensionStandalone() {} 9 | 10 | // When WireMock is run in standalone mode, WireMockServerRunner.run() is the entry point, 11 | // so we just delegate to that, passing along a CSV string with each extension class to load 12 | public static void main(String... args) { 13 | String[] newArgs = Arrays.copyOf(args, args.length + 1); 14 | newArgs[args.length] = "--extensions=" + JwtMatcherExtension.class.getName() + "," + JwtStubMappingTransformer.class.getName(); 15 | new WireMockServerRunner().run(newArgs); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /wiremock-jwt-extension.iml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /test-request-match.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -e 4 | 5 | source test-helpers.sh 6 | WIREMOCK_BASE_URL=http://localhost:8080 7 | 8 | launchWiremock 9 | 10 | echo -e "done\n\nCreating proxy mapping" 11 | curl -d@- http://localhost:8080/__admin/mappings <<-EOD 12 | { 13 | "request" : { 14 | "url" : "/some_url", 15 | "method" : "GET", 16 | "customMatcher" : { 17 | "name" : "jwt-matcher", 18 | "parameters" : { 19 | "header" : { 20 | "alg" : "HS256", 21 | "typ": "JWT" 22 | }, 23 | "payload": { 24 | "name" : "John Doe", 25 | "aud": ["foo", "bar"] 26 | } 27 | } 28 | } 29 | }, 30 | "response" : { 31 | "status" : 200, 32 | "body": "success" 33 | } 34 | } 35 | EOD 36 | 37 | echo -e "done\n\nMaking request" 38 | curl -H 'Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyLCJhdWQiOlsiZm9vIiwiYmFyIl19.aqa_OxjpGtC4nHVCUlCqmiNHOAYK6VFyq2HFsOOmJIY' http://localhost:8080/some_url 39 | -------------------------------------------------------------------------------- /test-stub-transformer.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -e 4 | 5 | source test-helpers.sh 6 | 7 | PROXY_BASE_URL="https://wiremock.org" 8 | WIREMOCK_BASE_URL=http://localhost:8080 9 | 10 | launchWiremock 11 | 12 | echo -e "done\n\nCreating proxy mapping" 13 | curl -s -d '{ 14 | "request": { "urlPattern": ".*" }, 15 | "response": { 16 | "proxyBaseUrl": "'${PROXY_BASE_URL}'" 17 | } 18 | }' http://localhost:8080/__admin/mappings > /dev/null 19 | 20 | 21 | echo -e "done\n\nMaking request" 22 | TEST_TOKEN='eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.XbPfbIHMI6arZ3Y922BhjWgQzWXcXNrz0ogtVhfEd2o' 23 | curl -s -H "Authorization: Bearer ${TEST_TOKEN}" "${WIREMOCK_BASE_URL}/robots.txt?foo=bar" > /dev/null 24 | 25 | REQUEST_JSON='{ 26 | "outputFormat": "full", 27 | "persist": false, 28 | "transformers": ["jwt-stub-mapping-transformer"], 29 | "transformerParameters": { 30 | "payloadFields": ["iat", "user"] 31 | }, 32 | "captureHeaders": { 33 | "Host": { "caseInsensitive": true }, 34 | "Authorization": { "caseInsensitive": true } 35 | }, 36 | "extractBodyCriteria": { 37 | "textSizeThreshold": "2000" 38 | } 39 | }' 40 | echo -e "done\n\nCalling snapshot API with '${REQUEST_JSON}'" 41 | curl -X POST -d "${REQUEST_JSON}" "${WIREMOCK_BASE_URL}/__admin/recordings/snapshot" 42 | -------------------------------------------------------------------------------- /src/main/java/com/github/masonm/Jwt.java: -------------------------------------------------------------------------------- 1 | package com.github.masonm; 2 | 3 | import org.apache.commons.codec.binary.Base64; 4 | import com.fasterxml.jackson.databind.JsonNode; 5 | import com.fasterxml.jackson.databind.ObjectMapper; 6 | import com.fasterxml.jackson.databind.node.MissingNode; 7 | 8 | import java.io.IOException; 9 | 10 | public class Jwt { 11 | private final JsonNode header; 12 | private final JsonNode payload; 13 | 14 | public Jwt(String token) { 15 | String[] parts = token.split("\\."); 16 | if (parts.length < 2) { 17 | this.header = MissingNode.getInstance(); 18 | this.payload = MissingNode.getInstance(); 19 | } else { 20 | this.header = parsePart(parts[0]); 21 | this.payload = parsePart(parts[1]); 22 | } 23 | } 24 | 25 | public static Jwt fromAuthHeader(String authHeader) { 26 | // Per RFC7235, the syntax for the credentials in the Authorization header is: 27 | // credentials = auth-scheme [ 1*SP ( token68 / #auth-param ) ] 28 | // where auth-scheme is usually "Bearer" for JWT, but some APIs use "JWT" instead. 29 | int separatorIndex = authHeader.indexOf(" "); 30 | if (separatorIndex == -1) { 31 | // Missing auth-scheme. Not standard, but try parsing it anyway. 32 | return new Jwt(authHeader); 33 | } else { 34 | return new Jwt(authHeader.substring(separatorIndex + 1)); 35 | } 36 | } 37 | 38 | private JsonNode parsePart(String part) { 39 | byte[] decodedJwtBody; 40 | try { 41 | decodedJwtBody = Base64.decodeBase64(part); 42 | } catch (IllegalArgumentException ex) { 43 | return MissingNode.getInstance(); 44 | } 45 | 46 | try { 47 | ObjectMapper mapper = new ObjectMapper(); 48 | return mapper.readValue(decodedJwtBody, JsonNode.class); 49 | } catch (IOException ioe) { 50 | return MissingNode.getInstance(); 51 | } 52 | } 53 | 54 | public JsonNode getPayload() { 55 | return payload; 56 | } 57 | 58 | public JsonNode getHeader() { 59 | return header; 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /src/main/java/com/github/masonm/JwtMatcherExtension.java: -------------------------------------------------------------------------------- 1 | package com.github.masonm; 2 | 3 | import com.fasterxml.jackson.core.type.TypeReference; 4 | import com.fasterxml.jackson.databind.JsonNode; 5 | import com.fasterxml.jackson.databind.ObjectMapper; 6 | import com.github.tomakehurst.wiremock.extension.Parameters; 7 | import com.github.tomakehurst.wiremock.http.Request; 8 | import com.github.tomakehurst.wiremock.matching.MatchResult; 9 | import com.github.tomakehurst.wiremock.matching.RequestMatcherExtension; 10 | import com.github.tomakehurst.wiremock.matching.RequestPattern; 11 | 12 | import java.util.Map; 13 | import java.util.Objects; 14 | 15 | import static com.github.tomakehurst.wiremock.matching.MatchResult.noMatch; 16 | import static com.github.tomakehurst.wiremock.matching.MatchResult.exactMatch; 17 | 18 | public class JwtMatcherExtension extends RequestMatcherExtension { 19 | public static final String NAME = "jwt-matcher"; 20 | public static final String PARAM_NAME_PAYLOAD = "payload"; 21 | public static final String PARAM_NAME_HEADER = "header"; 22 | public static final String PARAM_NAME_REQUEST = "request"; 23 | 24 | @Override 25 | public String getName() { 26 | return "jwt-matcher"; 27 | } 28 | 29 | @Override 30 | public MatchResult match(Request request, Parameters parameters) { 31 | if (!parameters.containsKey(PARAM_NAME_PAYLOAD) && !parameters.containsKey(PARAM_NAME_HEADER)) { 32 | return noMatch(); 33 | } 34 | 35 | if (parameters.containsKey(PARAM_NAME_REQUEST)) { 36 | Parameters requestParameters = Parameters.of(parameters.get(PARAM_NAME_REQUEST)); 37 | RequestPattern requestPattern = requestParameters.as(RequestPattern.class); 38 | if (!requestPattern.match(request).isExactMatch()) { 39 | return noMatch(); 40 | } 41 | } 42 | 43 | String authString = request.getHeader("Authorization"); 44 | if (authString == null || authString.isEmpty()) { 45 | return noMatch(); 46 | } 47 | 48 | Jwt token = Jwt.fromAuthHeader(authString); 49 | 50 | if ( 51 | parameters.containsKey(PARAM_NAME_HEADER) && 52 | !matchParams(token.getHeader(), parameters.get(PARAM_NAME_HEADER)) 53 | ) { 54 | return noMatch(); 55 | } 56 | 57 | if ( 58 | parameters.containsKey(PARAM_NAME_PAYLOAD) && 59 | !matchParams(token.getPayload(), parameters.get(PARAM_NAME_PAYLOAD)) 60 | ) { 61 | return noMatch(); 62 | } 63 | 64 | return exactMatch(); 65 | } 66 | 67 | private boolean matchParams(JsonNode tokenValues, Object parameters) { 68 | Map parameterMap = new ObjectMapper().convertValue( 69 | parameters, 70 | new TypeReference>() {} 71 | ); 72 | for (Map.Entry entry: parameterMap.entrySet()) { 73 | JsonNode tokenValue = tokenValues.path(entry.getKey()); 74 | if (!Objects.equals(tokenValue, entry.getValue())) { 75 | return false; 76 | } 77 | } 78 | return true; 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /gradlew.bat: -------------------------------------------------------------------------------- 1 | @rem 2 | @rem Copyright 2015 the original author or authors. 3 | @rem 4 | @rem Licensed under the Apache License, Version 2.0 (the "License"); 5 | @rem you may not use this file except in compliance with the License. 6 | @rem You may obtain a copy of the License at 7 | @rem 8 | @rem https://www.apache.org/licenses/LICENSE-2.0 9 | @rem 10 | @rem Unless required by applicable law or agreed to in writing, software 11 | @rem distributed under the License is distributed on an "AS IS" BASIS, 12 | @rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | @rem See the License for the specific language governing permissions and 14 | @rem limitations under the License. 15 | @rem 16 | 17 | @if "%DEBUG%"=="" @echo off 18 | @rem ########################################################################## 19 | @rem 20 | @rem Gradle startup script for Windows 21 | @rem 22 | @rem ########################################################################## 23 | 24 | @rem Set local scope for the variables with windows NT shell 25 | if "%OS%"=="Windows_NT" setlocal 26 | 27 | set DIRNAME=%~dp0 28 | if "%DIRNAME%"=="" set DIRNAME=. 29 | @rem This is normally unused 30 | set APP_BASE_NAME=%~n0 31 | set APP_HOME=%DIRNAME% 32 | 33 | @rem Resolve any "." and ".." in APP_HOME to make it shorter. 34 | for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi 35 | 36 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 37 | set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" 38 | 39 | @rem Find java.exe 40 | if defined JAVA_HOME goto findJavaFromJavaHome 41 | 42 | set JAVA_EXE=java.exe 43 | %JAVA_EXE% -version >NUL 2>&1 44 | if %ERRORLEVEL% equ 0 goto execute 45 | 46 | echo. 47 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 48 | echo. 49 | echo Please set the JAVA_HOME variable in your environment to match the 50 | echo location of your Java installation. 51 | 52 | goto fail 53 | 54 | :findJavaFromJavaHome 55 | set JAVA_HOME=%JAVA_HOME:"=% 56 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 57 | 58 | if exist "%JAVA_EXE%" goto execute 59 | 60 | echo. 61 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 62 | echo. 63 | echo Please set the JAVA_HOME variable in your environment to match the 64 | echo location of your Java installation. 65 | 66 | goto fail 67 | 68 | :execute 69 | @rem Setup the command line 70 | 71 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 72 | 73 | 74 | @rem Execute Gradle 75 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* 76 | 77 | :end 78 | @rem End local scope for the variables with windows NT shell 79 | if %ERRORLEVEL% equ 0 goto mainEnd 80 | 81 | :fail 82 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 83 | rem the _cmd.exe /c_ return code! 84 | set EXIT_CODE=%ERRORLEVEL% 85 | if %EXIT_CODE% equ 0 set EXIT_CODE=1 86 | if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE% 87 | exit /b %EXIT_CODE% 88 | 89 | :mainEnd 90 | if "%OS%"=="Windows_NT" endlocal 91 | 92 | :omega 93 | -------------------------------------------------------------------------------- /src/test/java/com/github/masonm/JwtTest.java: -------------------------------------------------------------------------------- 1 | package com.github.masonm; 2 | 3 | import com.fasterxml.jackson.databind.JsonNode; 4 | import com.fasterxml.jackson.databind.node.JsonNodeFactory; 5 | import com.fasterxml.jackson.databind.node.ObjectNode; 6 | import com.github.tomakehurst.wiremock.common.Encoding; 7 | import org.junit.Test; 8 | 9 | import java.util.Arrays; 10 | import java.util.List; 11 | 12 | import static org.hamcrest.MatcherAssert.assertThat; 13 | import static org.hamcrest.Matchers.is; 14 | 15 | public class JwtTest { 16 | private static final String UNSECURED_TEST_TOKEN = "eyJhbGciOiJub25lIn0.eyJuYW1lIjoiTWFzb24gTWFsb25lIn0."; 17 | private static final String SECURED_TEST_HEADER = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6Ik1hc29uIE1hbG9uZSIsImlhdCI6MTUxNjIzOTAyMn0.MAzWSeaKvOgZb8_uasPYK681tF7PfC0E2AmfDsLfefs"; 18 | 19 | @Test 20 | public void constructorUsesAbsentForInvalidTokens() { 21 | final List testValues = Arrays.asList( 22 | "", 23 | ".", 24 | "too.few_segments", 25 | "invalid.!!!!.base64", 26 | "too.many.segments.here", 27 | "Bearer a.b.c ", 28 | Encoding.encodeBase64("invalidJson".getBytes()) + "." + Encoding.encodeBase64("invalidJson".getBytes()) + ".foobar" 29 | ); 30 | for (String testValue: testValues) { 31 | Jwt token = new Jwt(testValue); 32 | String errMsg = "Failed with value '" + testValue + "'"; 33 | assertThat(errMsg, token.getHeader().isMissingNode(), is(true)); 34 | assertThat(errMsg, token.getPayload().isMissingNode(), is(true)); 35 | } 36 | } 37 | 38 | @Test 39 | public void fromAuthHeader() { 40 | final List testValues = Arrays.asList( 41 | "Bearer " + UNSECURED_TEST_TOKEN, 42 | "JWT " + UNSECURED_TEST_TOKEN, 43 | UNSECURED_TEST_TOKEN 44 | ); 45 | for (String testValue: testValues) { 46 | Jwt token = Jwt.fromAuthHeader(testValue); 47 | assertThat(token.getHeader().isMissingNode(), is(false)); 48 | assertThat(token.getPayload().isMissingNode(), is(false)); 49 | } 50 | } 51 | 52 | @Test 53 | public void withUnsecuredValidToken() { 54 | final Jwt token = new Jwt(UNSECURED_TEST_TOKEN); 55 | assertThat(token.getHeader().isMissingNode(), is(false)); 56 | assertThat(token.getPayload().isMissingNode(), is(false)); 57 | 58 | final ObjectNode headerRoot = JsonNodeFactory.instance.objectNode() 59 | .put("alg", "none"); 60 | assertThat(token.getHeader(), is((JsonNode) headerRoot)); 61 | 62 | final ObjectNode payloadRoot = JsonNodeFactory.instance.objectNode() 63 | .put("name", "Mason Malone"); 64 | assertThat(token.getPayload(), is((JsonNode) payloadRoot)); 65 | } 66 | 67 | @Test 68 | public void withSecuredValidToken() { 69 | final Jwt token = new Jwt(SECURED_TEST_HEADER); 70 | assertThat(token.getHeader().isMissingNode(), is(false)); 71 | assertThat(token.getPayload().isMissingNode(), is(false)); 72 | 73 | final ObjectNode headerRoot = JsonNodeFactory.instance.objectNode(); 74 | headerRoot.put("alg", "HS256"); 75 | headerRoot.put("typ", "JWT"); 76 | assertThat(token.getHeader(), is((JsonNode) headerRoot)); 77 | 78 | final ObjectNode payloadRoot = JsonNodeFactory.instance.objectNode(); 79 | payloadRoot.put("sub", "1234567890"); 80 | payloadRoot.put("name", "Mason Malone"); 81 | payloadRoot.put("iat", 1516239022); 82 | assertThat(token.getPayload(), is((JsonNode) payloadRoot)); 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /src/main/java/com/github/masonm/JwtStubMappingTransformer.java: -------------------------------------------------------------------------------- 1 | package com.github.masonm; 2 | 3 | import com.fasterxml.jackson.core.type.TypeReference; 4 | import com.fasterxml.jackson.databind.ObjectMapper; 5 | import com.github.tomakehurst.wiremock.common.FileSource; 6 | import com.github.tomakehurst.wiremock.extension.Parameters; 7 | import com.github.tomakehurst.wiremock.extension.StubMappingTransformer; 8 | import com.github.tomakehurst.wiremock.matching.CustomMatcherDefinition; 9 | import com.github.tomakehurst.wiremock.matching.MultiValuePattern; 10 | import com.github.tomakehurst.wiremock.matching.RequestPattern; 11 | import com.github.tomakehurst.wiremock.stubbing.StubMapping; 12 | 13 | import java.util.HashMap; 14 | import java.util.LinkedHashMap; 15 | import java.util.Map; 16 | 17 | import static com.google.common.net.HttpHeaders.AUTHORIZATION; 18 | 19 | public class JwtStubMappingTransformer extends StubMappingTransformer { 20 | public static final String PAYLOAD_FIELDS = "payloadFields"; 21 | 22 | @Override 23 | public String getName() { 24 | return "jwt-stub-mapping-transformer"; 25 | } 26 | 27 | @Override 28 | public boolean applyGlobally() { 29 | return false; 30 | } 31 | 32 | @Override 33 | public StubMapping transform(StubMapping stubMapping, FileSource files, Parameters parameters) { 34 | if (!parameters.containsKey(PAYLOAD_FIELDS)) { 35 | return stubMapping; 36 | } 37 | 38 | if (stubMapping.getRequest().getCustomMatcher() != null) { 39 | // already has a custom matcher. Don't overwrite 40 | return stubMapping; 41 | } 42 | 43 | Map requestHeaders = stubMapping.getRequest().getHeaders(); 44 | if (requestHeaders == null || !requestHeaders.containsKey(AUTHORIZATION)) { 45 | return stubMapping; 46 | } 47 | 48 | String authHeader = requestHeaders.get(AUTHORIZATION).getExpected(); 49 | Parameters requestMatcherParameters = getRequestMatcherParameter( 50 | Jwt.fromAuthHeader(authHeader), 51 | parameters.get(PAYLOAD_FIELDS) 52 | ); 53 | 54 | if (requestMatcherParameters == null) { 55 | return stubMapping; 56 | } 57 | 58 | CustomMatcherDefinition customMatcher = new CustomMatcherDefinition(JwtMatcherExtension.NAME, requestMatcherParameters); 59 | RequestPattern newRequest = this.getRequestPattern(stubMapping.getRequest(), customMatcher); 60 | stubMapping.setRequest(newRequest); 61 | return stubMapping; 62 | } 63 | 64 | private RequestPattern getRequestPattern(RequestPattern outer, CustomMatcherDefinition customMatcher) { 65 | Map newHeaders = null; 66 | if (outer.getHeaders() != null) { 67 | newHeaders = new LinkedHashMap<>(outer.getHeaders()); 68 | newHeaders.remove(AUTHORIZATION); 69 | if (newHeaders.isEmpty()) { 70 | newHeaders = null; 71 | } 72 | } 73 | 74 | return new RequestPattern( 75 | outer.getScheme(), 76 | outer.getHost(), 77 | outer.getPort(), 78 | outer.getUrlMatcher(), 79 | outer.getMethod(), 80 | newHeaders, 81 | outer.getPathParameters(), 82 | outer.getQueryParameters(), 83 | outer.getFormParameters(), 84 | outer.getCookies(), 85 | outer.getBasicAuthCredentials(), 86 | outer.getBodyPatterns(), 87 | customMatcher, 88 | null, 89 | outer.getMultipartPatterns() 90 | ); 91 | } 92 | 93 | private Parameters getRequestMatcherParameter(Jwt token, Object payloadParamValue) { 94 | if (token.getPayload().isMissingNode()) { 95 | return null; 96 | } 97 | 98 | Iterable payloadFields = new ObjectMapper().convertValue( 99 | payloadParamValue, 100 | new TypeReference>() {} 101 | ); 102 | Parameters params = new Parameters(); 103 | 104 | Map payload = new HashMap<>(); 105 | for (String field: payloadFields) { 106 | payload.put(field, token.getPayload().path(field).asText()); 107 | } 108 | params.put(JwtMatcherExtension.PARAM_NAME_PAYLOAD, payload); 109 | 110 | return params; 111 | } 112 | } 113 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Overview 2 | 3 | [![Build Status](https://github.com/MasonM/wiremock-jwt-extension/actions/workflows/gradle.yml/badge.svg)](https://github.com/MasonM/wiremock-jwt-extension/actions/workflows/gradle.yml?query=branch%3Amaster) 4 | [![Maven Central](https://maven-badges.herokuapp.com/maven-central/com.github.masonm/wiremock-jwt-extension/badge.svg)](https://maven-badges.herokuapp.com/maven-central/com.github.masonm/wiremock-jwt-extension) 5 | 6 | wiremock-jwt-extension consists of two extensions for [WireMock](http://wiremock.org): a [request matcher extension](http://wiremock.org/docs/extending-wiremock/#custom-request-matchers) and a [stub mapping transformer extension](http://wiremock.org/docs/record-playback/#transforming-generated-stubs). 7 | 8 | The request matcher extracts JWT tokens from incoming requests and matches against the "payload" and/or "header" portions. The stub mapping transformer can transform recorded stub mappings to use the request matcher if there exists a JWT token in the "Authorization" header. 9 | 10 | JWE (JSON Web Encryption) and signature verification are not currently supported. Patches welcome! 11 | 12 | # Installation 13 | 14 | Maven: 15 | ```xml 16 | 17 | com.github.masonm 18 | wiremock-jwt-extension 19 | 1.0.0 20 | 21 | ``` 22 | 23 | Gradle: 24 | ```groovy 25 | implementation 'com.github.masonm:wiremock-jwt-extension:1.0.0' 26 | ``` 27 | 28 | # Running 29 | 30 | There are three ways of running the extension: 31 | 32 | 1. Standalone, e.g. 33 | 34 | ```sh 35 | java -jar build/libs/wiremock-jwt-extension-1.0.0-standalone.jar 36 | ``` 37 | 38 | 2. As an extension of the WireMock standalone JAR, e.g. 39 | 40 | ```sh 41 | wget -nc https://repo1.maven.org/maven2/org/wiremock/wiremock-standalone/3.0.4/wiremock-standalone-3.0.4.jar 42 | java \ 43 | -cp wiremock-standalone-3.0.4.jar:build/libs/wiremock-jwt-extension-1.0.0.jar \ 44 | wiremock.Run \ 45 | --extensions="com.github.masonm.JwtMatcherExtension,com.github.masonm.JwtStubMappingTransformer" 46 | ``` 47 | 48 | 3. Programmatically in Java, e.g. 49 | 50 | ```java 51 | new WireMockServer(wireMockConfig() 52 | .extensions("com.github.masonm.JwtMatcherExtension", "com.github.masonm.JwtStubMappingTransformer")) 53 | ``` 54 | 55 | # Request matcher usage 56 | 57 | The extension accepts the following parameters: 58 | * `header`: Key-value map of header fields to match, e.g. `{ "alg": "HS256" }` 59 | * `payload`: Key-value map of payload fields to match, e.g. `{ "admin": true }`. If the value is an array (e.g. `{ "aud": ["aud1", "aud2"] }`, it will be matched exactly. 60 | * `request`: (legacy) Any additional request matchers. Only for Wiremock versions before 2.20 that lacked support for composing standard and custom matchers. 61 | 62 | When using the API, make sure to set the `"name"` field of the customMatcher to `"jwt-matcher"`. Here's an example cURL command that creates a stub mapping with the request matcher: 63 | ```sh 64 | curl -d@- http://localhost:8080/__admin/mappings <<-EOD 65 | { 66 | "request" : { 67 | "url" : "/some_url", 68 | "method" : "GET", 69 | "customMatcher" : { 70 | "name" : "jwt-matcher", 71 | "parameters" : { 72 | "header" : { 73 | "alg" : "HS256", 74 | "typ": "JWT" 75 | }, 76 | "payload": { 77 | "name" : "John Doe", 78 | "aud": ["aud1", "aud2"] 79 | } 80 | } 81 | } 82 | }, 83 | "response" : { 84 | "status" : 200, 85 | "body": "success" 86 | } 87 | } 88 | EOD 89 | ``` 90 | 91 | Example request that matches the above stub mapping: 92 | ```sh 93 | curl -H 'Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyLCJhdWQiOlsiYXVkMSIsImF1ZDIiXX0.h49E7AnYrJpttdEoi4GmoZUCtg6GBSHTSjUcDGnbjRI' http://localhost:8080/some_url 94 | ``` 95 | 96 | # Stub mapping transformer usage 97 | 98 | The transformer has the name "jwt-stub-mapping-transformer" and accepts a list of payload fields to match against via the parameter "payloadFields". Example request to `POST /__admin/recordings/snapshot`: 99 | ```json 100 | { 101 | "transformers" : [ "jwt-stub-mapping-transformer" ], 102 | "transformerParameters" : { 103 | "payloadFields" : [ "name", "admin" ] 104 | } 105 | } 106 | ``` 107 | 108 | # Building 109 | 110 | Run `gradle jar` to build the JAR without WireMock or `gradle standaloneJar` to build a standalone JAR. 111 | These will be placed in `build/libs/`. 112 | -------------------------------------------------------------------------------- /src/test/java/com/github/masonm/JwtMatcherExtensionTest.java: -------------------------------------------------------------------------------- 1 | package com.github.masonm; 2 | 3 | import com.github.tomakehurst.wiremock.extension.Parameters; 4 | import com.github.tomakehurst.wiremock.matching.MockRequest; 5 | import com.google.common.collect.ImmutableMap; 6 | import org.junit.Test; 7 | 8 | import static com.github.tomakehurst.wiremock.matching.MockRequest.mockRequest; 9 | import static org.junit.Assert.assertFalse; 10 | import static org.junit.Assert.assertTrue; 11 | 12 | public class JwtMatcherExtensionTest { 13 | private static final TestAuthHeader TEST_AUTH_HEADER = new TestAuthHeader( 14 | "{ \"test_header\": \"header_value\" }", 15 | "{ \"test_payload\": \"payload_value\" }" 16 | ); 17 | private static final Parameters PAYLOAD_PARAMETER = Parameters.one( 18 | JwtMatcherExtension.PARAM_NAME_PAYLOAD, 19 | ImmutableMap.of("test_payload", "payload_value") 20 | ); 21 | private static final Parameters HEADER_PARAMETER = Parameters.one( 22 | JwtMatcherExtension.PARAM_NAME_HEADER, 23 | ImmutableMap.of("test_header", "header_value") 24 | ); 25 | private static final Parameters BOTH_PARAMETERS = new Parameters() {{ 26 | putAll(PAYLOAD_PARAMETER); 27 | putAll(HEADER_PARAMETER); 28 | }}; 29 | 30 | @Test 31 | public void noMatchWithMissingRequiredParameters() { 32 | assertFalse(isExactMatch(mockRequest(), Parameters.empty())); 33 | 34 | Parameters invalidParameters = Parameters.one("test_header", "test_payload"); 35 | assertFalse(isExactMatch(mockRequest(), invalidParameters)); 36 | } 37 | 38 | @Test 39 | public void withValidParametersAndMatchingRequest() { 40 | final MockRequest request = mockRequest().header("Authorization", TEST_AUTH_HEADER.toString()); 41 | 42 | assertTrue(isExactMatch(request, PAYLOAD_PARAMETER)); 43 | assertTrue(isExactMatch(request, HEADER_PARAMETER)); 44 | assertTrue(isExactMatch(request, BOTH_PARAMETERS)); 45 | } 46 | 47 | @Test 48 | public void withValidParametersAndRequestWithoutAuthorization() { 49 | final MockRequest request = mockRequest(); 50 | assertFalse(isExactMatch(request, PAYLOAD_PARAMETER)); 51 | assertFalse(isExactMatch(request, HEADER_PARAMETER)); 52 | assertFalse(isExactMatch(request, BOTH_PARAMETERS)); 53 | } 54 | 55 | @Test 56 | public void withValidParametersAndRequestWithInvalidAuthorization() { 57 | final MockRequest request = mockRequest().header("Authorization", "Bearer f00"); 58 | assertFalse(isExactMatch(request, PAYLOAD_PARAMETER)); 59 | assertFalse(isExactMatch(request, HEADER_PARAMETER)); 60 | assertFalse(isExactMatch(request, BOTH_PARAMETERS)); 61 | } 62 | 63 | @Test 64 | public void withValidParametersAndNonMatchingRequest() { 65 | final MockRequest requestOnlyMatchingPayload = mockRequest() 66 | .header("Authorization", new TestAuthHeader( 67 | "{}", 68 | "{ \"test_payload\": \"payload_value\" }" 69 | ).toString()); 70 | assertFalse(isExactMatch(requestOnlyMatchingPayload, HEADER_PARAMETER)); 71 | assertFalse(isExactMatch(requestOnlyMatchingPayload, BOTH_PARAMETERS)); 72 | 73 | final MockRequest requestOnlyMatchingHeader = mockRequest() 74 | .header("Authorization", new TestAuthHeader( 75 | "{ \"test_header\": \"header_value\" }", 76 | "{}" 77 | ).toString()); 78 | assertFalse(isExactMatch(requestOnlyMatchingHeader, PAYLOAD_PARAMETER)); 79 | assertFalse(isExactMatch(requestOnlyMatchingHeader, BOTH_PARAMETERS)); 80 | } 81 | 82 | @Test 83 | public void withRequestParameter() { 84 | final Parameters requestAndBodyParameters = Parameters.from(PAYLOAD_PARAMETER); 85 | requestAndBodyParameters.put( 86 | "request", 87 | ImmutableMap.of("url", "/test_url") 88 | ); 89 | 90 | MockRequest testRequest = mockRequest() 91 | .url("/wrong_url") 92 | .header("Authorization", TEST_AUTH_HEADER.toString()); 93 | assertFalse(isExactMatch(testRequest, requestAndBodyParameters)); 94 | 95 | testRequest.url("/test_url"); 96 | assertTrue(isExactMatch(testRequest, requestAndBodyParameters)); 97 | } 98 | 99 | @Test 100 | public void withArrayPayload() { 101 | final TestAuthHeader authHeaderWithAud = new TestAuthHeader( 102 | "{ \"test_header\": \"header_value\" }", 103 | "{ \"aud\": [\"foo\", \"bar\"] }" 104 | ); 105 | final MockRequest request = mockRequest().header("Authorization", authHeaderWithAud.toString()); 106 | 107 | final Parameters matchPayloadParams = Parameters.one( 108 | JwtMatcherExtension.PARAM_NAME_PAYLOAD, 109 | ImmutableMap.of("aud", new String[] { "foo", "bar" }) 110 | ); 111 | assertTrue(isExactMatch(request, matchPayloadParams)); 112 | 113 | final Parameters noMatchPayloadParams = Parameters.one( 114 | JwtMatcherExtension.PARAM_NAME_PAYLOAD, 115 | ImmutableMap.of("aud", "foo") 116 | ); 117 | assertFalse(isExactMatch(request, noMatchPayloadParams)); 118 | } 119 | 120 | private boolean isExactMatch(MockRequest request, Parameters parameters) { 121 | return new JwtMatcherExtension().match(request.asLoggedRequest(), parameters).isExactMatch(); 122 | } 123 | } 124 | -------------------------------------------------------------------------------- /src/test/java/com/github/masonm/JwtStubMappingTransformerTest.java: -------------------------------------------------------------------------------- 1 | package com.github.masonm; 2 | 3 | import com.github.tomakehurst.wiremock.client.ResponseDefinitionBuilder; 4 | import com.github.tomakehurst.wiremock.client.WireMock; 5 | import com.github.tomakehurst.wiremock.common.Json; 6 | import com.github.tomakehurst.wiremock.extension.Parameters; 7 | import com.github.tomakehurst.wiremock.http.RequestMethod; 8 | import com.github.tomakehurst.wiremock.matching.RequestPattern; 9 | import com.github.tomakehurst.wiremock.stubbing.StubMapping; 10 | import com.google.common.collect.ImmutableMap; 11 | import org.junit.Test; 12 | import org.skyscreamer.jsonassert.JSONCompareMode; 13 | 14 | import java.util.Arrays; 15 | 16 | import static com.github.tomakehurst.wiremock.testsupport.WireMatchers.equalToJson; 17 | import static org.hamcrest.MatcherAssert.assertThat; 18 | import static org.hamcrest.Matchers.is; 19 | 20 | public class JwtStubMappingTransformerTest { 21 | private static final JwtStubMappingTransformer TRANSFORMER = new JwtStubMappingTransformer(); 22 | 23 | @Test 24 | public void returnsMappingUnmodifiedWithMissingParameters() { 25 | StubMapping testMapping = WireMock.get("/").build(); 26 | assertUnmodifiedOnTransform(testMapping, Parameters.empty()); 27 | } 28 | 29 | @Test 30 | public void returnsMappingUnmodifiedWhenRequestHasCustomMatcher() { 31 | StubMapping testMapping = WireMock.get("/").build(); 32 | testMapping.setRequest(WireMock.requestMadeFor(RequestPattern.ANYTHING).build()); 33 | assertUnmodifiedOnTransform(testMapping, getParams("foo")); 34 | } 35 | 36 | @Test 37 | public void returnsMappingUnmodifiedWhenNoAuthorizationHeader() { 38 | StubMapping testMapping = WireMock.get("/").build(); 39 | assertUnmodifiedOnTransform(testMapping, getParams("foo")); 40 | } 41 | 42 | @Test 43 | public void returnsMappingUnmodifiedWhenAuthorizationHeaderTypeIsNotBearer() { 44 | StubMapping testMapping = WireMock.get("/").withBasicAuth("foo", "bar").build(); 45 | assertUnmodifiedOnTransform(testMapping, getParams("foo")); 46 | } 47 | 48 | @Test 49 | public void returnsMappingUnmodifiedWhenAuthorizationHeaderIsInvalid() { 50 | StubMapping testMapping = WireMock 51 | .get("/") 52 | .withHeader("Authorization", WireMock.equalTo("Bearer f00")) 53 | .build(); 54 | assertUnmodifiedOnTransform(testMapping, getParams("foo")); 55 | } 56 | 57 | @Test 58 | public void returnsModifiedMappingWhenMatchingValidPayloadField() { 59 | final TestAuthHeader testAuthHeader = new TestAuthHeader( 60 | "doesnt_matter", 61 | "{ \"matched_key\": \"matched_value\" }" 62 | ); 63 | StubMapping testMapping = WireMock 64 | .post("/") 65 | .withHeader("Authorization", WireMock.equalTo(testAuthHeader.toString())) 66 | .build(); 67 | final Parameters payloadMatchParams = Parameters.one( 68 | JwtStubMappingTransformer.PAYLOAD_FIELDS, 69 | Arrays.asList("matched_key") 70 | ); 71 | 72 | StubMapping transformedMapping = TRANSFORMER.transform(testMapping, null, payloadMatchParams); 73 | RequestPattern actualRequestPattern = transformedMapping.getRequest(); 74 | 75 | final Parameters expectedParameters = Parameters.one( 76 | JwtMatcherExtension.PARAM_NAME_PAYLOAD, 77 | ImmutableMap.of("matched_key", "matched_value") 78 | ); 79 | assertThat(actualRequestPattern.getCustomMatcher().getName(), is(JwtMatcherExtension.NAME)); 80 | assertThat(actualRequestPattern.getCustomMatcher().getParameters(), is(expectedParameters)); 81 | 82 | final RequestPattern expectedRequestPattern = new RequestPattern( 83 | null, 84 | null, 85 | null, 86 | WireMock.urlEqualTo("/"), 87 | RequestMethod.POST, 88 | null, 89 | null, 90 | null, 91 | null, 92 | null, 93 | null, 94 | null, 95 | actualRequestPattern.getCustomMatcher(), 96 | null, 97 | null 98 | ); 99 | assertThat(actualRequestPattern, is(expectedRequestPattern)); 100 | } 101 | 102 | @Test 103 | public void acceptanceTestReturnsModifiedMappingWhenMatchingValidPayloadField() { 104 | final TestAuthHeader testAuthHeader = new TestAuthHeader( 105 | "doesnt_matter", 106 | "{ \"matched_key\": \"matched_value\" }" 107 | ); 108 | StubMapping testMapping = WireMock 109 | .get("/") 110 | .withHeader("Host", WireMock.equalTo("www.example.com")) 111 | .withHeader("Authorization", WireMock.equalTo(testAuthHeader.toString())) 112 | .willReturn(ResponseDefinitionBuilder.okForJson("foo")) 113 | .build(); 114 | final Parameters payloadMatchParams = Parameters.one( 115 | JwtStubMappingTransformer.PAYLOAD_FIELDS, 116 | Arrays.asList("matched_key") 117 | ); 118 | 119 | StubMapping transformedMapping = TRANSFORMER.transform(testMapping, null, payloadMatchParams); 120 | final String stubMappingJson = Json.write(transformedMapping); 121 | final String EXPECTED_STUB_MAPPING_JSON = 122 | "{\n" + 123 | "\"request\": {\n" + 124 | "\"url\": \"/\",\n" + 125 | "\"method\": \"GET\",\n" + 126 | "\"headers\": {\n" + 127 | "\"Host\": { \"equalTo\": \"www.example.com\" }\n" + 128 | "},\n" + 129 | "\"customMatcher\": {\n" + 130 | "\"name\": \"" + JwtMatcherExtension.NAME + "\",\n" + 131 | "\"parameters\": {\n" + 132 | "\"payload\": {\n" + 133 | "\"matched_key\": \"matched_value\"\n" + 134 | "}\n" + 135 | "}\n" + 136 | "}\n" + 137 | "},\n" + 138 | "\"response\": {\n" + 139 | "\"status\": 200,\n" + 140 | "\"body\": \"\\\"foo\\\"\",\n" + 141 | "\"headers\": {\n" + 142 | "\"Content-Type\": \"application/json\"\n" + 143 | "}\n" + 144 | "}\n" + 145 | "}"; 146 | assertThat(stubMappingJson, equalToJson(EXPECTED_STUB_MAPPING_JSON, JSONCompareMode.STRICT_ORDER)); 147 | } 148 | 149 | private Parameters getParams(String ...payloadFields) { 150 | return Parameters.one(JwtStubMappingTransformer.PAYLOAD_FIELDS, Arrays.asList(payloadFields)); 151 | } 152 | 153 | private void assertUnmodifiedOnTransform(StubMapping testMapping, Parameters parameters) { 154 | String json = testMapping.toString(); 155 | TRANSFORMER.transform(testMapping, null, parameters); 156 | assertThat(testMapping.toString(), is(json)); 157 | } 158 | } 159 | -------------------------------------------------------------------------------- /gradlew: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | # 4 | # Copyright © 2015-2021 the original authors. 5 | # 6 | # Licensed under the Apache License, Version 2.0 (the "License"); 7 | # you may not use this file except in compliance with the License. 8 | # You may obtain a copy of the License at 9 | # 10 | # https://www.apache.org/licenses/LICENSE-2.0 11 | # 12 | # Unless required by applicable law or agreed to in writing, software 13 | # distributed under the License is distributed on an "AS IS" BASIS, 14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | # See the License for the specific language governing permissions and 16 | # limitations under the License. 17 | # 18 | 19 | ############################################################################## 20 | # 21 | # Gradle start up script for POSIX generated by Gradle. 22 | # 23 | # Important for running: 24 | # 25 | # (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is 26 | # noncompliant, but you have some other compliant shell such as ksh or 27 | # bash, then to run this script, type that shell name before the whole 28 | # command line, like: 29 | # 30 | # ksh Gradle 31 | # 32 | # Busybox and similar reduced shells will NOT work, because this script 33 | # requires all of these POSIX shell features: 34 | # * functions; 35 | # * expansions «$var», «${var}», «${var:-default}», «${var+SET}», 36 | # «${var#prefix}», «${var%suffix}», and «$( cmd )»; 37 | # * compound commands having a testable exit status, especially «case»; 38 | # * various built-in commands including «command», «set», and «ulimit». 39 | # 40 | # Important for patching: 41 | # 42 | # (2) This script targets any POSIX shell, so it avoids extensions provided 43 | # by Bash, Ksh, etc; in particular arrays are avoided. 44 | # 45 | # The "traditional" practice of packing multiple parameters into a 46 | # space-separated string is a well documented source of bugs and security 47 | # problems, so this is (mostly) avoided, by progressively accumulating 48 | # options in "$@", and eventually passing that to Java. 49 | # 50 | # Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS, 51 | # and GRADLE_OPTS) rely on word-splitting, this is performed explicitly; 52 | # see the in-line comments for details. 53 | # 54 | # There are tweaks for specific operating systems such as AIX, CygWin, 55 | # Darwin, MinGW, and NonStop. 56 | # 57 | # (3) This script is generated from the Groovy template 58 | # https://github.com/gradle/gradle/blob/HEAD/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt 59 | # within the Gradle project. 60 | # 61 | # You can find Gradle at https://github.com/gradle/gradle/. 62 | # 63 | ############################################################################## 64 | 65 | # Attempt to set APP_HOME 66 | 67 | # Resolve links: $0 may be a link 68 | app_path=$0 69 | 70 | # Need this for daisy-chained symlinks. 71 | while 72 | APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path 73 | [ -h "$app_path" ] 74 | do 75 | ls=$( ls -ld "$app_path" ) 76 | link=${ls#*' -> '} 77 | case $link in #( 78 | /*) app_path=$link ;; #( 79 | *) app_path=$APP_HOME$link ;; 80 | esac 81 | done 82 | 83 | # This is normally unused 84 | # shellcheck disable=SC2034 85 | APP_BASE_NAME=${0##*/} 86 | # Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036) 87 | APP_HOME=$( cd "${APP_HOME:-./}" > /dev/null && pwd -P ) || exit 88 | 89 | # Use the maximum available, or set MAX_FD != -1 to use that value. 90 | MAX_FD=maximum 91 | 92 | warn () { 93 | echo "$*" 94 | } >&2 95 | 96 | die () { 97 | echo 98 | echo "$*" 99 | echo 100 | exit 1 101 | } >&2 102 | 103 | # OS specific support (must be 'true' or 'false'). 104 | cygwin=false 105 | msys=false 106 | darwin=false 107 | nonstop=false 108 | case "$( uname )" in #( 109 | CYGWIN* ) cygwin=true ;; #( 110 | Darwin* ) darwin=true ;; #( 111 | MSYS* | MINGW* ) msys=true ;; #( 112 | NONSTOP* ) nonstop=true ;; 113 | esac 114 | 115 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar 116 | 117 | 118 | # Determine the Java command to use to start the JVM. 119 | if [ -n "$JAVA_HOME" ] ; then 120 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 121 | # IBM's JDK on AIX uses strange locations for the executables 122 | JAVACMD=$JAVA_HOME/jre/sh/java 123 | else 124 | JAVACMD=$JAVA_HOME/bin/java 125 | fi 126 | if [ ! -x "$JAVACMD" ] ; then 127 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 128 | 129 | Please set the JAVA_HOME variable in your environment to match the 130 | location of your Java installation." 131 | fi 132 | else 133 | JAVACMD=java 134 | if ! command -v java >/dev/null 2>&1 135 | then 136 | die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 137 | 138 | Please set the JAVA_HOME variable in your environment to match the 139 | location of your Java installation." 140 | fi 141 | fi 142 | 143 | # Increase the maximum file descriptors if we can. 144 | if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then 145 | case $MAX_FD in #( 146 | max*) 147 | # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked. 148 | # shellcheck disable=SC3045 149 | MAX_FD=$( ulimit -H -n ) || 150 | warn "Could not query maximum file descriptor limit" 151 | esac 152 | case $MAX_FD in #( 153 | '' | soft) :;; #( 154 | *) 155 | # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked. 156 | # shellcheck disable=SC3045 157 | ulimit -n "$MAX_FD" || 158 | warn "Could not set maximum file descriptor limit to $MAX_FD" 159 | esac 160 | fi 161 | 162 | # Collect all arguments for the java command, stacking in reverse order: 163 | # * args from the command line 164 | # * the main class name 165 | # * -classpath 166 | # * -D...appname settings 167 | # * --module-path (only if needed) 168 | # * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. 169 | 170 | # For Cygwin or MSYS, switch paths to Windows format before running java 171 | if "$cygwin" || "$msys" ; then 172 | APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) 173 | CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" ) 174 | 175 | JAVACMD=$( cygpath --unix "$JAVACMD" ) 176 | 177 | # Now convert the arguments - kludge to limit ourselves to /bin/sh 178 | for arg do 179 | if 180 | case $arg in #( 181 | -*) false ;; # don't mess with options #( 182 | /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath 183 | [ -e "$t" ] ;; #( 184 | *) false ;; 185 | esac 186 | then 187 | arg=$( cygpath --path --ignore --mixed "$arg" ) 188 | fi 189 | # Roll the args list around exactly as many times as the number of 190 | # args, so each arg winds up back in the position where it started, but 191 | # possibly modified. 192 | # 193 | # NB: a `for` loop captures its iteration list before it begins, so 194 | # changing the positional parameters here affects neither the number of 195 | # iterations, nor the values presented in `arg`. 196 | shift # remove old arg 197 | set -- "$@" "$arg" # push replacement arg 198 | done 199 | fi 200 | 201 | 202 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 203 | DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' 204 | 205 | # Collect all arguments for the java command; 206 | # * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of 207 | # shell script including quotes and variable substitutions, so put them in 208 | # double quotes to make sure that they get re-expanded; and 209 | # * put everything else in single quotes, so that it's not re-expanded. 210 | 211 | set -- \ 212 | "-Dorg.gradle.appname=$APP_BASE_NAME" \ 213 | -classpath "$CLASSPATH" \ 214 | org.gradle.wrapper.GradleWrapperMain \ 215 | "$@" 216 | 217 | # Stop when "xargs" is not available. 218 | if ! command -v xargs >/dev/null 2>&1 219 | then 220 | die "xargs is not available" 221 | fi 222 | 223 | # Use "xargs" to parse quoted args. 224 | # 225 | # With -n1 it outputs one arg per line, with the quotes and backslashes removed. 226 | # 227 | # In Bash we could simply go: 228 | # 229 | # readarray ARGS < <( xargs -n1 <<<"$var" ) && 230 | # set -- "${ARGS[@]}" "$@" 231 | # 232 | # but POSIX shell has neither arrays nor command substitution, so instead we 233 | # post-process each arg (as a line of input to sed) to backslash-escape any 234 | # character that might be a shell metacharacter, then use eval to reverse 235 | # that process (while maintaining the separation between arguments), and wrap 236 | # the whole thing up as a single "set" statement. 237 | # 238 | # This will of course break if any of these variables contains a newline or 239 | # an unmatched quote. 240 | # 241 | 242 | eval "set -- $( 243 | printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | 244 | xargs -n1 | 245 | sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | 246 | tr '\n' ' ' 247 | )" '"$@"' 248 | 249 | exec "$JAVACMD" "$@" 250 | --------------------------------------------------------------------------------