diff --git a/.gitignore b/.gitignore index 2165313e71..bc8a1f650d 100644 --- a/.gitignore +++ b/.gitignore @@ -73,3 +73,4 @@ notes.txt # MINE # Guide.txt docker +src/main/resources/wiremock-gui-version.properties diff --git a/.idea/icon.svg b/.idea/icon.svg new file mode 100644 index 0000000000..20788b6866 --- /dev/null +++ b/.idea/icon.svg @@ -0,0 +1,87 @@ + + + + + + + + + + + + + + + diff --git a/build.gradle b/build.gradle index 3311a8ca6b..b77d97947c 100644 --- a/build.gradle +++ b/build.gradle @@ -12,6 +12,7 @@ buildscript { plugins { id 'java-library' + id 'java-test-fixtures' id 'scala' id 'signing' id 'maven-publish' @@ -23,7 +24,8 @@ plugins { id 'com.github.johnrengelman.shadow' version '8.1.1' id "org.sonarqube" version "4.4.1.3373" id 'jacoco' - id 'com.github.node-gradle.node' version '7.0.1' + id "me.champeau.jmh" version "0.7.2" + id 'com.github.node-gradle.node' version '7.0.2' } group = 'org.wiremock' @@ -32,8 +34,8 @@ project.ext { versions = [ handlebars : '4.3.1', jetty : '11.0.20', - guava : '33.0.0-jre', - jackson : '2.16.1', + guava : '33.1.0-jre', + jackson : '2.17.0', xmlUnit : '2.9.1', jsonUnit : '2.38.0', junitJupiter: '5.10.2' @@ -58,7 +60,7 @@ dependencies { api "org.eclipse.jetty:jetty-alpn-java-client" api "org.eclipse.jetty:jetty-alpn-client" - api "org.eclipse.jetty.websocket:websocket-jakarta-server" + api "org.eclipse.jetty.websocket:websocket-jakarta-server:$versions.jetty" api "io.jsonwebtoken:jjwt-api:0.11.5" api "io.jsonwebtoken:jjwt-impl:0.11.5" @@ -83,7 +85,6 @@ dependencies { api "com.jayway.jsonpath:json-path:2.9.0", { exclude group: 'org.ow2.asm', module: 'asm' } - api "org.ow2.asm:asm:9.6" implementation "org.slf4j:slf4j-api:1.7.36" standaloneOnly "org.slf4j:slf4j-nop:1.7.36" @@ -96,60 +97,52 @@ dependencies { compileOnly(platform("org.junit:junit-bom:$versions.junitJupiter")) compileOnly("org.junit.jupiter:junit-jupiter") - api 'org.apache.commons:commons-lang3:3.14.0' api "com.github.jknack:handlebars:$versions.handlebars", { exclude group: 'org.mozilla', module: 'rhino' } api("com.github.jknack:handlebars-helpers:$versions.handlebars") { exclude group: 'org.mozilla', module: 'rhino' + exclude group: 'org.apache.commons', module: 'commons-lang3' } api 'commons-fileupload:commons-fileupload:1.5', { exclude group: 'commons-io', module: 'commons-io' } - api "commons-io:commons-io:2.15.1" - - api 'com.networknt:json-schema-validator:1.3.3' - - testImplementation "junit:junit:4.13" - testImplementation("org.junit.jupiter:junit-jupiter:$versions.junitJupiter") - testImplementation("org.junit.platform:junit-platform-testkit") + api "commons-io:commons-io:2.16.0" + + api 'com.networknt:json-schema-validator:1.4.0' + + testFixturesApi("org.junit.jupiter:junit-jupiter:$versions.junitJupiter") + testFixturesApi("org.junit.platform:junit-platform-testkit") + testFixturesApi("org.junit.platform:junit-platform-launcher") + testFixturesApi("org.junit.jupiter:junit-jupiter-params") + testFixturesApi('org.junit-pioneer:junit-pioneer:2.2.0') + testFixturesApi "org.hamcrest:hamcrest-core:2.2" + testFixturesApi "org.hamcrest:hamcrest-library:2.2" + testFixturesApi 'org.mockito:mockito-core:5.11.0' + testFixturesApi 'org.mockito:mockito-junit-jupiter:5.11.0' + testFixturesApi "net.javacrumbs.json-unit:json-unit:$versions.jsonUnit" + testFixturesApi "org.skyscreamer:jsonassert:1.5.1" + testFixturesApi 'com.toomuchcoding.jsonassert:jsonassert:0.8.0' + testFixturesApi 'org.awaitility:awaitility:4.2.1' + testFixturesApi "commons-io:commons-io:2.16.0" + + testImplementation "junit:junit:4.13.2" testRuntimeOnly("org.junit.vintage:junit-vintage-engine") - testImplementation("org.junit.platform:junit-platform-launcher") - testImplementation("org.junit.jupiter:junit-jupiter-params") - testImplementation('org.junit-pioneer:junit-pioneer:2.2.0') - - testImplementation "org.hamcrest:hamcrest-core:2.2" - testImplementation "org.hamcrest:hamcrest-library:2.2" - testImplementation 'org.mockito:mockito-core:5.10.0' - testImplementation 'org.mockito:mockito-junit-jupiter:5.10.0' - testImplementation "net.javacrumbs.json-unit:json-unit:$versions.jsonUnit" - testImplementation "org.skyscreamer:jsonassert:1.2.3" - testImplementation 'com.toomuchcoding.jsonassert:jsonassert:0.8.0' - testImplementation 'org.awaitility:awaitility:4.2.0' - testImplementation "com.googlecode.jarjar:jarjar:1.3" - testImplementation "commons-io:commons-io:2.15.1" testImplementation 'org.scala-lang:scala-library:2.13.13' testImplementation 'com.tngtech.archunit:archunit-junit5:0.23.1' testImplementation "org.eclipse.jetty:jetty-client" testImplementation "org.eclipse.jetty.http2:http2-http-client-transport" - testRuntimeOnly "org.slf4j:log4j-over-slf4j:2.0.12" - testRuntimeOnly "ch.qos.logback:logback-classic:1.4.0" testRuntimeOnly files('src/test/resources/classpath file source/classpathfiles.zip', 'src/test/resources/classpath-filesource.jar') - testImplementation('net.jockx:littleproxy:1.1.3') { - exclude group: 'com.google.guava', module: 'guava' - exclude group: 'org.apache.commons', module: 'commons-lang3' - exclude group: 'org.slf4j', module: 'slf4j-api' - exclude group: 'io.netty', module: 'netty-all' - } - testImplementation "io.netty:netty-all:4.1.107.Final" - testImplementation files('test-extension/test-extension.jar') + testImplementation 'org.openjdk.jmh:jmh-core:1.37' + testImplementation 'org.openjdk.jmh:jmh-generator-annprocess:1.37' + constraints { - implementation "net.minidev:json-smart:2.5.0", { + implementation "net.minidev:json-smart:2.5.1", { because 'Pinning this above the transitive version from json-path to get CVE fix' } } @@ -226,10 +219,6 @@ allprojects { version = '1.0.0-Snapshot' } - - sourceCompatibility = 11 - targetCompatibility = 11 - compileJava { options.encoding = 'UTF-8' @@ -244,6 +233,10 @@ allprojects { options.compilerArgs += '--add-exports=java.base/sun.security.x509=ALL-UNNAMED' } + compileTestFixturesJava { + options.encoding = 'UTF-8' + } + test { // Set the timezone for testing somewhere other than my machine to increase the chances of catching timezone bugs systemProperty 'user.timezone', 'Australia/Sydney' @@ -277,7 +270,11 @@ allprojects { shadowJar.dependsOn jar } +test.classpath += sourceSets.main.compileClasspath + sourceSets.main.runtimeClasspath + java { + sourceCompatibility = 11 + targetCompatibility = 11 withSourcesJar() withJavadocJar() } @@ -376,36 +373,55 @@ publishing { } } + components.java.withVariantsFromConfiguration(configurations.testFixturesApiElements) { skip() } + components.java.withVariantsFromConfiguration(configurations.testFixturesRuntimeElements) { skip() } + getComponents().withType(AdhocComponentWithVariants).each { c -> c.withVariantsFromConfiguration(project.configurations.shadowRuntimeElements) { skip() } } - publications { - mavenJava(MavenPublication) { publication -> - artifactId = "${jar.getArchiveBaseName().get()}" - from components.java - artifact testJar + if (JavaVersion.current().isJava11()) { + publications { + mavenJava(MavenPublication) { publication -> + artifactId = "${jar.getArchiveBaseName().get()}" + from components.java + artifact testJar - pom.withXml { - asNode().appendNode('description', 'A web service test double for all occasions') - asNode().children().last() + pomInfo + pom.withXml { + asNode().appendNode('description', 'A web service test double for all occasions') + asNode().children().last() + pomInfo + } } - } - standaloneJar(MavenPublication) { publication -> - artifactId = "${jar.getArchiveBaseName().get()}-standalone" - project.shadow.component(publication) + standaloneJar(MavenPublication) { publication -> + artifactId = "${jar.getArchiveBaseName().get()}-standalone" + project.shadow.component(publication) - artifact sourcesJar - artifact javadocJar - artifact testJar + artifact sourcesJar + artifact javadocJar + artifact testJar - pom.packaging 'jar' - pom.withXml { - asNode().appendNode('description', 'A web service test double for all occasions - standalone edition') - asNode().children().last() + pomInfo + pom.packaging 'jar' + pom.withXml { + asNode().appendNode('description', 'A web service test double for all occasions - standalone edition') + asNode().children().last() + pomInfo + } + } + } + } + + nexusPublishing { + // See https://github.com/wiremock/community/blob/main/infra/maven-central.md + repositories { + sonatype { + def envUsername = providers.environmentVariable("OSSRH_USERNAME").orElse("").get() + def envPassword = providers.environmentVariable("OSSRH_TOKEN").orElse("").get() + if (!envUsername.isEmpty() && !envPassword.isEmpty()) { + username.set(envUsername) + password.set(envPassword) + } } } } @@ -416,7 +432,6 @@ task checkReleasePreconditions { def REQUIRED_GIT_BRANCH = 'master' def currentGitBranch = 'git rev-parse --abbrev-ref HEAD'.execute().text.trim() assert currentGitBranch == REQUIRED_GIT_BRANCH, "Must be on the $REQUIRED_GIT_BRANCH branch in order to release to Sonatype" - assert JavaVersion.current().isJava11(), 'Must use Java 8 when releasing' } } @@ -443,8 +458,21 @@ task generateWebapp(type: NpmTask) { args = ['run', 'prod'] } -task finalizeWebapp(type: Copy) { +task versionTxt() { dependsOn generateWebapp + doLast { + def wiremockVersionProps = new Properties() + wiremockVersionProps.load(new FileInputStream(projectDir.getAbsolutePath() + "/src/main/resources/version.properties")) + def wiremockVersion = wiremockVersionProps.getProperty("version") + new File(projectDir.getAbsolutePath() + "/src/main/resources", "wiremock-gui-version.properties").text = """# version file +gui-version=$version +version=$wiremockVersion +""" + } +} + +task finalizeWebapp(type: Copy) { + dependsOn versionTxt // Because of: https://github.com/angular/angular-cli/issues/26304 // we need to copy some things around. from 'webapp/wiremock/dist/browser' @@ -593,3 +621,8 @@ wrapper { gradleVersion = '8.6' distributionType = Wrapper.DistributionType.BIN } + +jmh { + includes = ['.*benchmarks.*'] + threads = 50 +} diff --git a/perf-test/src/main/java/wiremock/LoadTestConfiguration.java b/perf-test/src/main/java/wiremock/LoadTestConfiguration.java index c3c5943e5d..50233ad017 100644 --- a/perf-test/src/main/java/wiremock/LoadTestConfiguration.java +++ b/perf-test/src/main/java/wiremock/LoadTestConfiguration.java @@ -20,7 +20,6 @@ import static com.github.tomakehurst.wiremock.client.WireMock.*; import static java.util.concurrent.TimeUnit.SECONDS; -import static org.apache.commons.lang3.RandomStringUtils.randomAscii; public class LoadTestConfiguration { diff --git a/sample-war/src/main/webappCustomMapping/WEB-INF/wiremock/__files/.gitignore b/sample-war/src/main/webappCustomMapping/WEB-INF/wiremock/__files/.gitignore new file mode 100644 index 0000000000..e69de29bb2 diff --git a/sample-war/src/main/webappLimitedRequestJournal/WEB-INF/wiremock/__files/.gitignore b/sample-war/src/main/webappLimitedRequestJournal/WEB-INF/wiremock/__files/.gitignore new file mode 100644 index 0000000000..e69de29bb2 diff --git a/settings.gradle b/settings.gradle index 39952cb59f..9faf3ef478 100644 --- a/settings.gradle +++ b/settings.gradle @@ -1 +1,4 @@ rootProject.name = 'wiremock' +if (JavaVersion.current() >= JavaVersion.VERSION_17) { + include 'wiremock-jetty12' +} diff --git a/src/main/java/com/github/tomakehurst/wiremock/WireMockServer.java b/src/main/java/com/github/tomakehurst/wiremock/WireMockServer.java index d0937a9e52..37c8f485f1 100644 --- a/src/main/java/com/github/tomakehurst/wiremock/WireMockServer.java +++ b/src/main/java/com/github/tomakehurst/wiremock/WireMockServer.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2011-2023 Thomas Akehurst + * Copyright (C) 2011-2024 Thomas Akehurst * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -28,6 +28,7 @@ import com.github.tomakehurst.wiremock.core.Container; import com.github.tomakehurst.wiremock.core.Options; import com.github.tomakehurst.wiremock.core.WireMockApp; +import com.github.tomakehurst.wiremock.extension.Extension; import com.github.tomakehurst.wiremock.global.GlobalSettings; import com.github.tomakehurst.wiremock.http.HttpServer; import com.github.tomakehurst.wiremock.http.HttpServerFactory; @@ -49,8 +50,10 @@ import com.github.tomakehurst.wiremock.stubbing.StubMappingJsonRecorder; import com.github.tomakehurst.wiremock.verification.*; import java.util.List; +import java.util.ServiceLoader; import java.util.Set; import java.util.UUID; +import org.eclipse.jetty.util.Jetty; public class WireMockServer implements Container, Stubbing, Admin { @@ -72,10 +75,7 @@ public WireMockServer(Options options) { this.stubRequestHandler = wireMockApp.buildStubRequestHandler(); - HttpServerFactory httpServerFactory = - wireMockApp.getExtensions().ofType(HttpServerFactory.class).values().stream() - .findFirst() - .orElseGet(options::httpServerFactory); + HttpServerFactory httpServerFactory = getHttpServerFactory(); httpServer = httpServerFactory.buildHttpServer( @@ -84,6 +84,31 @@ public WireMockServer(Options options) { client = new WireMock(wireMockApp); } + private HttpServerFactory getHttpServerFactory() { + if (!options.isExtensionScanningEnabled() && !isJetty11()) { + return ServiceLoader.load(Extension.class).stream() + .filter(extension -> HttpServerFactory.class.isAssignableFrom(extension.type())) + .findFirst() + .map(e -> (HttpServerFactory) e.get()) + .orElseThrow( + () -> + new FatalStartupException( + "Jetty 11 is not present and no suitable HttpServerFactory extension was found. Please ensure that the classpath includes a WireMock extension that provides an HttpServerFactory implementation. See http://wiremock.org/docs/extending-wiremock/ for more information.")); + } + + return wireMockApp.getExtensions().ofType(HttpServerFactory.class).values().stream() + .findFirst() + .orElseGet(options::httpServerFactory); + } + + private static boolean isJetty11() { + try { + return Jetty.VERSION.startsWith("11"); + } catch (Throwable e) { + return false; + } + } + public WireMockServer( int port, Integer httpsPort, @@ -259,6 +284,11 @@ public void removeStub(StubMapping stubMapping) { client.removeStubMapping(stubMapping); } + @Override + public void removeStub(UUID id) { + client.removeStubMapping(id); + } + @Override public List getStubMappings() { return client.allStubMappings().getMappings(); diff --git a/src/main/java/com/github/tomakehurst/wiremock/admin/model/VersionResult.java b/src/main/java/com/github/tomakehurst/wiremock/admin/model/VersionResult.java index ed3e2c75cd..e79a2fdb94 100644 --- a/src/main/java/com/github/tomakehurst/wiremock/admin/model/VersionResult.java +++ b/src/main/java/com/github/tomakehurst/wiremock/admin/model/VersionResult.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2023 Thomas Akehurst + * Copyright (C) 2023-2024 Thomas Akehurst * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -18,11 +18,18 @@ public class VersionResult { private final String version; - public VersionResult(String version) { + private final String guiVersion; + + public VersionResult(String version, String guiVersion) { this.version = version; + this.guiVersion = guiVersion; } public String getVersion() { return version; } + + public String getGuiVersion() { + return guiVersion; + } } diff --git a/src/main/java/com/github/tomakehurst/wiremock/admin/tasks/GetVersionTask.java b/src/main/java/com/github/tomakehurst/wiremock/admin/tasks/GetVersionTask.java index f9aaa4d55e..2066f24697 100644 --- a/src/main/java/com/github/tomakehurst/wiremock/admin/tasks/GetVersionTask.java +++ b/src/main/java/com/github/tomakehurst/wiremock/admin/tasks/GetVersionTask.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2023 Thomas Akehurst + * Copyright (C) 2023-2024 Thomas Akehurst * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -44,7 +44,7 @@ public ResponseDefinition execute(Admin admin, ServeEvent serveEvent, PathParams .build(); } - var versionResult = new VersionResult(Version.getCurrentVersion()); + var versionResult = new VersionResult(Version.getCurrentVersion(), Version.getGuiVersion()); return jsonResponse(versionResult); } } diff --git a/src/main/java/com/github/tomakehurst/wiremock/client/HttpAdminClient.java b/src/main/java/com/github/tomakehurst/wiremock/client/HttpAdminClient.java index e4a7672e71..2f84211456 100644 --- a/src/main/java/com/github/tomakehurst/wiremock/client/HttpAdminClient.java +++ b/src/main/java/com/github/tomakehurst/wiremock/client/HttpAdminClient.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2011-2023 Thomas Akehurst + * Copyright (C) 2011-2024 Thomas Akehurst * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -17,6 +17,7 @@ import static com.github.tomakehurst.wiremock.common.Exceptions.throwUnchecked; import static com.github.tomakehurst.wiremock.common.HttpClientUtils.getEntityAsStringAndCloseStream; +import static com.github.tomakehurst.wiremock.common.Strings.isNotBlank; import static com.github.tomakehurst.wiremock.security.NoClientAuthenticator.noClientAuthenticator; import static java.util.Objects.requireNonNull; import static org.apache.hc.core5.http.HttpHeaders.HOST; @@ -49,7 +50,6 @@ import java.util.List; import java.util.Optional; import java.util.UUID; -import org.apache.commons.lang3.StringUtils; import org.apache.hc.client5.http.classic.methods.HttpGet; import org.apache.hc.client5.http.classic.methods.HttpPost; import org.apache.hc.client5.http.classic.methods.HttpPut; @@ -454,7 +454,7 @@ public int port() { } private ProxySettings createProxySettings(String proxyHost, int proxyPort) { - if (StringUtils.isNotBlank(proxyHost)) { + if (isNotBlank(proxyHost)) { return new ProxySettings(proxyHost, proxyPort); } return ProxySettings.NO_PROXY; diff --git a/src/main/java/com/github/tomakehurst/wiremock/client/ResponseDefinitionBuilder.java b/src/main/java/com/github/tomakehurst/wiremock/client/ResponseDefinitionBuilder.java index f687cce246..bcdd05f476 100644 --- a/src/main/java/com/github/tomakehurst/wiremock/client/ResponseDefinitionBuilder.java +++ b/src/main/java/com/github/tomakehurst/wiremock/client/ResponseDefinitionBuilder.java @@ -79,6 +79,10 @@ public static ResponseDefinitionBuilder like(ResponseDefinition responseDefiniti responseDefinition.getAdditionalProxyRequestHeaders() != null ? (List) responseDefinition.getAdditionalProxyRequestHeaders().all() : new ArrayList<>(); + proxyResponseDefinitionBuilder.removeRequestHeaders = + responseDefinition.getRemoveProxyRequestHeaders() != null + ? responseDefinition.getRemoveProxyRequestHeaders() + : new ArrayList<>(); return proxyResponseDefinitionBuilder; } @@ -232,6 +236,7 @@ public ResponseDefinitionBuilder withStatusMessage(String message) { public static class ProxyResponseDefinitionBuilder extends ResponseDefinitionBuilder { private List additionalRequestHeaders = new ArrayList<>(); + private List removeRequestHeaders = new ArrayList<>(); public ProxyResponseDefinitionBuilder(ResponseDefinitionBuilder from) { this.status = from.status; @@ -254,6 +259,11 @@ public ProxyResponseDefinitionBuilder withAdditionalRequestHeader(String key, St return this; } + public ProxyResponseDefinitionBuilder withRemoveRequestHeader(String key) { + removeRequestHeaders.add(key.toLowerCase()); + return this; + } + public ProxyResponseDefinitionBuilder withProxyUrlPrefixToRemove( String proxyUrlPrefixToRemove) { this.proxyUrlPrefixToRemove = proxyUrlPrefixToRemove; @@ -264,6 +274,7 @@ public ProxyResponseDefinitionBuilder withProxyUrlPrefixToRemove( public ResponseDefinition build() { return super.build( !additionalRequestHeaders.isEmpty() ? new HttpHeaders(additionalRequestHeaders) : null, + !removeRequestHeaders.isEmpty() ? removeRequestHeaders : null, proxyUrlPrefixToRemove); } } @@ -274,11 +285,13 @@ public ResponseDefinitionBuilder withFault(Fault fault) { } public ResponseDefinition build() { - return build(null, null); + return build(null, null, null); } protected ResponseDefinition build( - HttpHeaders additionalProxyRequestHeaders, String proxyUrlPrefixToRemove) { + HttpHeaders additionalProxyRequestHeaders, + List removeProxyRequestHeaders, + String proxyUrlPrefixToRemove) { HttpHeaders httpHeaders = headers == null || headers.isEmpty() ? null : new HttpHeaders(headers); Parameters transformerParameters = @@ -292,6 +305,7 @@ protected ResponseDefinition build( bodyFileName, httpHeaders, additionalProxyRequestHeaders, + removeProxyRequestHeaders, fixedDelayMilliseconds, delayDistribution, chunkedDribbleDelay, diff --git a/src/main/java/com/github/tomakehurst/wiremock/client/WireMock.java b/src/main/java/com/github/tomakehurst/wiremock/client/WireMock.java index 9a3b28f227..6860f86d68 100644 --- a/src/main/java/com/github/tomakehurst/wiremock/client/WireMock.java +++ b/src/main/java/com/github/tomakehurst/wiremock/client/WireMock.java @@ -146,6 +146,10 @@ public static void removeStub(StubMapping stubMapping) { defaultInstance.get().removeStubMapping(stubMapping); } + public static void removeStub(UUID id) { + defaultInstance.get().removeStubMapping(id); + } + public static ListStubMappingsResult listAllStubMappings() { return defaultInstance.get().allStubMappings(); } @@ -458,6 +462,10 @@ public void removeStubMapping(StubMapping stubMapping) { admin.removeStubMapping(stubMapping); } + public void removeStubMapping(UUID id) { + admin.removeStubMapping(id); + } + public ListStubMappingsResult allStubMappings() { return admin.listAllStubMappings(); } diff --git a/src/main/java/com/github/tomakehurst/wiremock/common/ContentTypes.java b/src/main/java/com/github/tomakehurst/wiremock/common/ContentTypes.java index 05a70e5e4c..3541e0a29f 100644 --- a/src/main/java/com/github/tomakehurst/wiremock/common/ContentTypes.java +++ b/src/main/java/com/github/tomakehurst/wiremock/common/ContentTypes.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2016-2023 Thomas Akehurst + * Copyright (C) 2016-2024 Thomas Akehurst * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,9 +16,9 @@ package com.github.tomakehurst.wiremock.common; import static com.github.tomakehurst.wiremock.common.Strings.stringFromBytes; +import static com.github.tomakehurst.wiremock.common.Strings.substringAfterLast; import static com.github.tomakehurst.wiremock.common.TextType.JSON; import static java.util.Arrays.asList; -import static org.apache.commons.lang3.StringUtils.substringAfterLast; import com.fasterxml.jackson.databind.JsonNode; import com.github.tomakehurst.wiremock.common.xml.Xml; diff --git a/src/main/java/com/github/tomakehurst/wiremock/common/FatalStartupException.java b/src/main/java/com/github/tomakehurst/wiremock/common/FatalStartupException.java index 9a3f786b21..c64fa9ac41 100644 --- a/src/main/java/com/github/tomakehurst/wiremock/common/FatalStartupException.java +++ b/src/main/java/com/github/tomakehurst/wiremock/common/FatalStartupException.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2011-2021 Thomas Akehurst + * Copyright (C) 2011-2024 Thomas Akehurst * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -17,6 +17,10 @@ public class FatalStartupException extends RuntimeException { + public FatalStartupException(String message) { + super(message); + } + public FatalStartupException(Throwable cause) { super(cause); } diff --git a/src/main/java/com/github/tomakehurst/wiremock/common/Gzip.java b/src/main/java/com/github/tomakehurst/wiremock/common/Gzip.java index 604fb85361..376f7e80f8 100644 --- a/src/main/java/com/github/tomakehurst/wiremock/common/Gzip.java +++ b/src/main/java/com/github/tomakehurst/wiremock/common/Gzip.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2015-2023 Thomas Akehurst + * Copyright (C) 2015-2024 Thomas Akehurst * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,8 +16,8 @@ package com.github.tomakehurst.wiremock.common; import static com.github.tomakehurst.wiremock.common.Exceptions.throwUnchecked; -import static com.github.tomakehurst.wiremock.common.Strings.DEFAULT_CHARSET; import static com.github.tomakehurst.wiremock.common.Strings.bytesFromString; +import static java.nio.charset.StandardCharsets.UTF_8; import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; @@ -49,7 +49,7 @@ public static String unGzipToString(byte[] gzippedContent) { } public static byte[] gzip(String plainContent) { - return gzip(plainContent, DEFAULT_CHARSET); + return gzip(plainContent, UTF_8); } public static byte[] gzip(String plainContent, Charset charset) { diff --git a/src/main/java/com/github/tomakehurst/wiremock/common/ListFunctions.java b/src/main/java/com/github/tomakehurst/wiremock/common/ListFunctions.java index ff81c1b6ee..3f3a16b12d 100644 --- a/src/main/java/com/github/tomakehurst/wiremock/common/ListFunctions.java +++ b/src/main/java/com/github/tomakehurst/wiremock/common/ListFunctions.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2020-2021 Thomas Akehurst + * Copyright (C) 2020-2024 Thomas Akehurst * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -17,6 +17,8 @@ import java.util.ArrayList; import java.util.List; +import java.util.stream.Collectors; +import java.util.stream.Stream; public final class ListFunctions { @@ -33,6 +35,11 @@ public static Pair, List> splitByType(A[] items, Cla return new Pair<>(as, bs); } + @SafeVarargs + public static List concatenate(List... lists) { + return Stream.of(lists).flatMap(List::stream).collect(Collectors.toList()); + } + private ListFunctions() { throw new UnsupportedOperationException("Not instantiable"); } diff --git a/src/main/java/com/github/tomakehurst/wiremock/common/ParameterUtils.java b/src/main/java/com/github/tomakehurst/wiremock/common/ParameterUtils.java index 478d958303..2b5e3ba3cf 100644 --- a/src/main/java/com/github/tomakehurst/wiremock/common/ParameterUtils.java +++ b/src/main/java/com/github/tomakehurst/wiremock/common/ParameterUtils.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2023 Thomas Akehurst + * Copyright (C) 2023-2024 Thomas Akehurst * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -33,6 +33,16 @@ public static T getFirstNonNull(T first, T second) { throw new NullPointerException("Both parameters are null"); } + public static T getFirstNonNull(T first, T second, String etr) { + if (first != null) { + return first; + } + if (second != null) { + return second; + } + throw new NullPointerException(etr); + } + public static void checkParameter(boolean condition, String errorMessage) { if (!condition) { throw new IllegalArgumentException(errorMessage); diff --git a/src/main/java/com/github/tomakehurst/wiremock/common/ProxySettings.java b/src/main/java/com/github/tomakehurst/wiremock/common/ProxySettings.java index 9850336d54..7749f20bcb 100644 --- a/src/main/java/com/github/tomakehurst/wiremock/common/ProxySettings.java +++ b/src/main/java/com/github/tomakehurst/wiremock/common/ProxySettings.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2013-2023 Thomas Akehurst + * Copyright (C) 2013-2024 Thomas Akehurst * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,7 +16,7 @@ package com.github.tomakehurst.wiremock.common; import static com.github.tomakehurst.wiremock.common.ParameterUtils.checkParameter; -import static org.apache.commons.lang3.StringUtils.isEmpty; +import static com.github.tomakehurst.wiremock.common.Strings.isNotEmpty; import java.net.MalformedURLException; import java.net.URL; @@ -54,7 +54,7 @@ public static ProxySettings fromString(String config) { ProxySettings proxySettings = new ProxySettings( proxyUrl.getHost(), proxyUrl.getPort() == -1 ? DEFAULT_PORT : proxyUrl.getPort()); - if (!isEmpty(proxyUrl.getUserInfo())) { + if (isNotEmpty(proxyUrl.getUserInfo())) { String[] userInfoArray = proxyUrl.getUserInfo().split(":"); proxySettings.setUsername(userInfoArray[0]); if (userInfoArray.length > 1) { @@ -99,6 +99,6 @@ public String toString() { } return String.format( - "%s:%s%s", host(), port(), (!isEmpty(this.username) ? " (with credentials)" : "")); + "%s:%s%s", host(), port(), (isNotEmpty(this.username) ? " (with credentials)" : "")); } } diff --git a/src/main/java/com/github/tomakehurst/wiremock/extension/responsetemplating/RenderCache.java b/src/main/java/com/github/tomakehurst/wiremock/common/RequestCache.java similarity index 63% rename from src/main/java/com/github/tomakehurst/wiremock/extension/responsetemplating/RenderCache.java rename to src/main/java/com/github/tomakehurst/wiremock/common/RequestCache.java index d868a0e367..8dc4131460 100644 --- a/src/main/java/com/github/tomakehurst/wiremock/extension/responsetemplating/RenderCache.java +++ b/src/main/java/com/github/tomakehurst/wiremock/common/RequestCache.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2020-2021 Thomas Akehurst + * Copyright (C) 2020-2024 Thomas Akehurst * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package com.github.tomakehurst.wiremock.extension.responsetemplating; +package com.github.tomakehurst.wiremock.common; import static java.util.Arrays.asList; @@ -21,8 +21,45 @@ import java.util.List; import java.util.Map; import java.util.Objects; +import java.util.function.Supplier; -public class RenderCache { +public class RequestCache { + + private static RequestCache OFF = + new RequestCache() { + @Override + public void put(Key key, Object value) {} + + @Override + public T get(Key key) { + return null; + } + + @Override + public T get(Key key, Supplier supplier) { + return supplier.get(); + } + }; + + private static final ThreadLocal current = new ThreadLocal<>(); + + public static RequestCache getCurrent() { + RequestCache requestCache = current.get(); + if (requestCache == null) { + requestCache = new RequestCache(); + current.set(requestCache); + } + + return requestCache; + } + + public static void onRequestEnd() { + current.remove(); + } + + public static void disable() { + current.set(OFF); + } private final Map cache = new HashMap<>(); @@ -35,6 +72,11 @@ public T get(Key key) { return (T) cache.get(key); } + @SuppressWarnings("unchecked") + public T get(Key key, Supplier supplier) { + return (T) cache.computeIfAbsent(key, k -> supplier.get()); + } + public static class Key { private final Class forClass; private final List elements; diff --git a/src/main/java/com/github/tomakehurst/wiremock/common/Strings.java b/src/main/java/com/github/tomakehurst/wiremock/common/Strings.java index 35b0ebe45d..67ff208bae 100644 --- a/src/main/java/com/github/tomakehurst/wiremock/common/Strings.java +++ b/src/main/java/com/github/tomakehurst/wiremock/common/Strings.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2015-2023 Thomas Akehurst + * Copyright (C) 2015-2024 Thomas Akehurst * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -15,21 +15,273 @@ */ package com.github.tomakehurst.wiremock.common; +import static java.lang.Math.max; import static java.lang.System.lineSeparator; import static java.nio.charset.StandardCharsets.UTF_8; -import static org.apache.commons.lang3.StringUtils.getLevenshteinDistance; import java.nio.charset.Charset; -import org.apache.commons.lang3.text.WordUtils; +import java.util.Arrays; +import java.util.Random; +import java.util.concurrent.ThreadLocalRandom; +import java.util.regex.Matcher; +import java.util.regex.Pattern; public class Strings { private Strings() {} - public static final Charset DEFAULT_CHARSET = UTF_8; + private static ThreadLocalRandom random() { + return ThreadLocalRandom.current(); + } + + public static int getLevenshteinDistance(CharSequence s, CharSequence t) { + if (s == null || t == null) { + throw new IllegalArgumentException("Strings must not be null"); + } + + int n = s.length(); + int m = t.length(); + + if (n == 0) { + return m; + } + if (m == 0) { + return n; + } + + if (n > m) { + // swap the input strings to consume less memory + final CharSequence tmp = s; + s = t; + t = tmp; + n = m; + m = t.length(); + } + + final int[] p = new int[n + 1]; + // indexes into strings s and t + int i; // iterates through s + int j; // iterates through t + int upperleft; + int upper; + + char jOfT; // jth character of t + int cost; + + for (i = 0; i <= n; i++) { + p[i] = i; + } + + for (j = 1; j <= m; j++) { + upperleft = p[0]; + jOfT = t.charAt(j - 1); + p[0] = j; + + for (i = 1; i <= n; i++) { + upper = p[i]; + cost = s.charAt(i - 1) == jOfT ? 0 : 1; + // minimum of cell to the left+1, to the top+1, diagonally left and up +cost + p[i] = Math.min(Math.min(p[i - 1] + 1, p[i] + 1), upperleft + cost); + upperleft = upper; + } + } + + return p[n]; + } + + public static String randomAlphanumeric(final int count) { + return random(count, true, true); + } + + public static String random(final int count, final boolean letters, final boolean numbers) { + return random(count, 0, 0, letters, numbers); + } + + public static String randomAlphabetic(final int count) { + return random(count, true, false); + } + + public static String randomNumeric(final int count) { + return random(count, false, true); + } + + public static String random(final int count, final String chars) { + if (chars == null) { + return random(count, 0, 0, false, false, null, random()); + } + return random(count, chars.toCharArray()); + } + + public static String random(final int count, final char... chars) { + if (chars == null) { + return random(count, 0, 0, false, false, null, random()); + } + return random(count, 0, chars.length, false, false, chars, random()); + } + + public static String randomAscii(final int count) { + return random(count, 32, 127, false, false); + } + + public static String random( + final int count, + final int start, + final int end, + final boolean letters, + final boolean numbers) { + return random(count, start, end, letters, numbers, null, random()); + } + + public static String random( + int count, + int start, + int end, + final boolean letters, + final boolean numbers, + final char[] chars, + final Random random) { + if (count == 0) { + return ""; + } + if (count < 0) { + throw new IllegalArgumentException( + "Requested random string length " + count + " is less than 0."); + } + if (chars != null && chars.length == 0) { + throw new IllegalArgumentException("The chars array must not be empty"); + } + + if (start == 0 && end == 0) { + if (chars != null) { + end = chars.length; + } else if (!letters && !numbers) { + end = Character.MAX_CODE_POINT; + } else { + end = 'z' + 1; + start = ' '; + } + } else if (end <= start) { + throw new IllegalArgumentException( + "Parameter end (" + end + ") must be greater than start (" + start + ")"); + } + + final int zeroDigitAscii = 48; + final int firstLetterAscii = 65; + + if (chars == null && (numbers && end <= zeroDigitAscii || letters && end <= firstLetterAscii)) { + throw new IllegalArgumentException( + "Parameter end (" + + end + + ") must be greater then (" + + zeroDigitAscii + + ") for generating digits " + + "or greater then (" + + firstLetterAscii + + ") for generating letters."); + } + + final StringBuilder builder = new StringBuilder(count); + final int gap = end - start; + + while (count-- != 0) { + final int codePoint; + if (chars == null) { + codePoint = random.nextInt(gap) + start; + + switch (Character.getType(codePoint)) { + case Character.UNASSIGNED: + case Character.PRIVATE_USE: + case Character.SURROGATE: + count++; + continue; + } + + } else { + codePoint = chars[random.nextInt(gap) + start]; + } + + final int numberOfChars = Character.charCount(codePoint); + if (count == 0 && numberOfChars > 1) { + count++; + continue; + } + + if (letters && Character.isLetter(codePoint) + || numbers && Character.isDigit(codePoint) + || !letters && !numbers) { + builder.appendCodePoint(codePoint); + + if (numberOfChars == 2) { + count--; + } + + } else { + count++; + } + } + return builder.toString(); + } + + public static String rightPad(final String str, final int size) { + return rightPad(str, size, ' '); + } + + public static String rightPad(final String str, final int size, final char padChar) { + if (str == null) { + return null; + } + final int pads = size - str.length(); + if (pads <= 0) { + return str; // returns original String when possible + } + if (pads > 8192) { + return rightPad(str, size, String.valueOf(padChar)); + } + return str.concat(repeat(padChar, pads)); + } + + public static String rightPad(final String str, final int size, String padStr) { + if (str == null) { + return null; + } + if (isEmpty(padStr)) { + padStr = " "; + } + final int padLen = padStr.length(); + final int strLen = str.length(); + final int pads = size - strLen; + if (pads <= 0) { + return str; // returns original String when possible + } + if (padLen == 1 && pads <= 8192) { + return rightPad(str, size, padStr.charAt(0)); + } + + if (pads == padLen) { + return str.concat(padStr); + } + if (pads < padLen) { + return str.concat(padStr.substring(0, pads)); + } + final char[] padding = new char[pads]; + final char[] padChars = padStr.toCharArray(); + for (int i = 0; i < pads; i++) { + padding[i] = padChars[i % padLen]; + } + return str.concat(new String(padding)); + } + + public static String repeat(final char ch, final int repeat) { + if (repeat <= 0) { + return ""; + } + final char[] buf = new char[repeat]; + Arrays.fill(buf, ch); + return new String(buf); + } public static String stringFromBytes(byte[] bytes) { - return stringFromBytes(bytes, DEFAULT_CHARSET); + return stringFromBytes(bytes, UTF_8); } public static String stringFromBytes(byte[] bytes, Charset charset) { @@ -41,7 +293,7 @@ public static String stringFromBytes(byte[] bytes, Charset charset) { } public static byte[] bytesFromString(String str) { - return bytesFromString(str, DEFAULT_CHARSET); + return bytesFromString(str, UTF_8); } public static byte[] bytesFromString(String str, Charset charset) { @@ -55,13 +307,179 @@ public static byte[] bytesFromString(String str, Charset charset) { public static String wrapIfLongestLineExceedsLimit(String s, int maxLineLength) { int longestLength = findLongestLineLength(s); if (longestLength > maxLineLength) { - String wrapped = WordUtils.wrap(s, maxLineLength, null, true); + String wrapped = wrap(s, maxLineLength, null, true); return wrapped.replaceAll("(?m)^[ \t]*\r?\n", ""); } return s; } + public static String wrap( + final String str, + final int wrapLength, + final String newLineStr, + final boolean wrapLongWords) { + return wrap(str, wrapLength, newLineStr, wrapLongWords, " "); + } + + public static String wrap( + final String str, + int wrapLength, + String newLineStr, + final boolean wrapLongWords, + String wrapOn) { + if (str == null) { + return null; + } + if (newLineStr == null) { + newLineStr = System.lineSeparator(); + } + if (wrapLength < 1) { + wrapLength = 1; + } + if (isBlank(wrapOn)) { + wrapOn = " "; + } + final Pattern patternToWrapOn = Pattern.compile(wrapOn); + final int inputLineLength = str.length(); + int offset = 0; + final StringBuilder wrappedLine = new StringBuilder(inputLineLength + 32); + + while (offset < inputLineLength) { + int spaceToWrapAt = -1; + Matcher matcher = + patternToWrapOn.matcher( + str.substring( + offset, + Math.min( + (int) Math.min(Integer.MAX_VALUE, offset + wrapLength + 1L), + inputLineLength))); + if (matcher.find()) { + if (matcher.start() == 0) { + offset += matcher.end(); + continue; + } + spaceToWrapAt = matcher.start() + offset; + } + + // only last line without leading spaces is left + if (inputLineLength - offset <= wrapLength) { + break; + } + + while (matcher.find()) { + spaceToWrapAt = matcher.start() + offset; + } + + if (spaceToWrapAt >= offset) { + // normal case + wrappedLine.append(str, offset, spaceToWrapAt); + wrappedLine.append(newLineStr); + offset = spaceToWrapAt + 1; + + } else // really long word or URL + if (wrapLongWords) { + // wrap really long word one line at a time + wrappedLine.append(str, offset, wrapLength + offset); + wrappedLine.append(newLineStr); + offset += wrapLength; + } else { + // do not wrap really long word, just extend beyond limit + matcher = patternToWrapOn.matcher(str.substring(offset + wrapLength)); + if (matcher.find()) { + spaceToWrapAt = matcher.start() + offset + wrapLength; + } + + if (spaceToWrapAt >= 0) { + wrappedLine.append(str, offset, spaceToWrapAt); + wrappedLine.append(newLineStr); + offset = spaceToWrapAt + 1; + } else { + wrappedLine.append(str, offset, str.length()); + offset = inputLineLength; + } + } + } + + // Whatever is left in line is short enough to just pass through + wrappedLine.append(str, offset, str.length()); + + return wrappedLine.toString(); + } + + public static String substringAfterLast(final String str, final int separator) { + if (isEmpty(str)) { + return str; + } + final int pos = str.lastIndexOf(separator); + if (pos == -1 || pos == str.length() - 1) { + return ""; + } + return str.substring(pos + 1); + } + + public static String substringAfterLast(final String str, final String separator) { + if (isEmpty(str)) { + return str; + } + if (isEmpty(separator)) { + return ""; + } + final int pos = str.lastIndexOf(separator); + if (pos == -1 || pos == str.length() - separator.length()) { + return ""; + } + return str.substring(pos + separator.length()); + } + + public static int countMatches(final CharSequence str, final char ch) { + if (isEmpty(str)) { + return 0; + } + int count = 0; + // We could also call str.toCharArray() for faster lookups but that would generate more garbage. + for (int i = 0; i < str.length(); i++) { + if (ch == str.charAt(i)) { + count++; + } + } + return count; + } + + public static int ordinalIndexOf( + final CharSequence str, final CharSequence searchStr, final int ordinal) { + return ordinalIndexOf(str, searchStr, ordinal, false); + } + + private static int ordinalIndexOf( + final CharSequence str, + final CharSequence searchStr, + final int ordinal, + final boolean lastIndex) { + if (str == null || searchStr == null || ordinal <= 0) { + return -1; + } + if (searchStr.length() == 0) { + return lastIndex ? str.length() : 0; + } + int found = 0; + // set the initial index beyond the end of the string + // this is to allow for the initial index decrement/increment + int index = lastIndex ? str.length() : -1; + do { + if (lastIndex) { + index = lastIndexOf(str, searchStr, index - 1); // step backwards through string + } else { + index = indexOf(str, searchStr, index + 1); // step forwards through string + } + if (index < 0) { + return index; + } + found++; + } while (found < ordinal); + return index; + } + private static int findLongestLineLength(String s) { String[] lines = s.split("\n"); int longestLength = 0; @@ -80,7 +498,7 @@ public static double normalisedLevenshteinDistance(String one, String two) { return 1.0; } - double maxDistance = Math.max(one.length(), two.length()); + double maxDistance = max(one.length(), two.length()); double actualDistance = getLevenshteinDistance(one, two); return (actualDistance / maxDistance); } @@ -90,6 +508,140 @@ public static String normaliseLineBreaks(String s) { } public static boolean isNullOrEmpty(String s) { - return s == null || s.isEmpty(); + return isNull(s) || s.isEmpty(); + } + + public static boolean isNotNullOrEmpty(String s) { + return !isNullOrEmpty(s); + } + + public static boolean isBlank(String s) { + return isNull(s) || s.isBlank(); + } + + public static boolean isNotBlank(String s) { + return !isBlank(s); + } + + public static boolean isNull(String s) { + return s == null; + } + + public static boolean isNotNull(String s) { + return !isNull(s); + } + + public static boolean isEmpty(CharSequence charSequence) { + return charSequence == null || charSequence.length() == 0; + } + + public static boolean isEmpty(String s) { + return isNull(s) || s.isEmpty(); + } + + public static boolean isNotEmpty(String s) { + return !isEmpty(s); + } + + public static String removeStart(String str, String remove) { + if (isEmpty(str) || isEmpty(remove)) { + return str; + } + if (str.startsWith(remove)) { + return str.substring(remove.length()); + } + return str; + } + + private static boolean checkLaterThan1( + final CharSequence cs, final CharSequence searchChar, final int len2, final int start1) { + for (int i = 1, j = len2 - 1; i <= j; i++, j--) { + if (cs.charAt(start1 + i) != searchChar.charAt(i) + || cs.charAt(start1 + j) != searchChar.charAt(j)) { + return false; + } + } + return true; + } + + private static int indexOf( + final CharSequence cs, final CharSequence searchChar, final int start) { + if (cs instanceof String) { + return ((String) cs).indexOf(searchChar.toString(), start); + } + if (cs instanceof StringBuilder) { + return ((StringBuilder) cs).indexOf(searchChar.toString(), start); + } + if (cs instanceof StringBuffer) { + return ((StringBuffer) cs).indexOf(searchChar.toString(), start); + } + return cs.toString().indexOf(searchChar.toString(), start); + } + + private static int lastIndexOf(final CharSequence cs, final CharSequence searchChar, int start) { + if (searchChar == null || cs == null) { + return -1; + } + if (searchChar instanceof String) { + if (cs instanceof String) { + return ((String) cs).lastIndexOf((String) searchChar, start); + } + if (cs instanceof StringBuilder) { + return ((StringBuilder) cs).lastIndexOf((String) searchChar, start); + } + if (cs instanceof StringBuffer) { + return ((StringBuffer) cs).lastIndexOf((String) searchChar, start); + } + } + + final int len1 = cs.length(); + final int len2 = searchChar.length(); + + if (start > len1) { + start = len1; + } + + if (start < 0 || len2 > len1) { + return -1; + } + + if (len2 == 0) { + return start; + } + + if (len2 <= 16) { + if (cs instanceof String) { + return ((String) cs).lastIndexOf(searchChar.toString(), start); + } + if (cs instanceof StringBuilder) { + return ((StringBuilder) cs).lastIndexOf(searchChar.toString(), start); + } + if (cs instanceof StringBuffer) { + return ((StringBuffer) cs).lastIndexOf(searchChar.toString(), start); + } + } + + if (start + len2 > len1) { + start = len1 - len2; + } + + final char char0 = searchChar.charAt(0); + + int i = start; + while (true) { + while (cs.charAt(i) != char0) { + i--; + if (i < 0) { + return -1; + } + } + if (checkLaterThan1(cs, searchChar, len2, i)) { + return i; + } + i--; + if (i < 0) { + return -1; + } + } } } diff --git a/src/main/java/com/github/tomakehurst/wiremock/common/UniqueFilenameGenerator.java b/src/main/java/com/github/tomakehurst/wiremock/common/UniqueFilenameGenerator.java index 5b5f89c028..2ee42dcf18 100644 --- a/src/main/java/com/github/tomakehurst/wiremock/common/UniqueFilenameGenerator.java +++ b/src/main/java/com/github/tomakehurst/wiremock/common/UniqueFilenameGenerator.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2013-2023 Thomas Akehurst + * Copyright (C) 2013-2024 Thomas Akehurst * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,7 +16,6 @@ package com.github.tomakehurst.wiremock.common; import java.net.URI; -import org.apache.commons.lang3.StringUtils; public class UniqueFilenameGenerator { @@ -29,7 +28,7 @@ public static String generate(String url, String prefix, String id, String exten pathPart = pathPart.isBlank() ? "(root)" : sanitise(pathPart); if (pathPart.length() > 150) { - pathPart = StringUtils.truncate(pathPart, 150); + pathPart = pathPart.substring(0, 150); } return prefix + "-" + pathPart + "-" + id + "." + extension; diff --git a/src/main/java/com/github/tomakehurst/wiremock/common/Urls.java b/src/main/java/com/github/tomakehurst/wiremock/common/Urls.java index 77f77924ef..1e4934a0a6 100644 --- a/src/main/java/com/github/tomakehurst/wiremock/common/Urls.java +++ b/src/main/java/com/github/tomakehurst/wiremock/common/Urls.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2011-2023 Thomas Akehurst + * Copyright (C) 2011-2024 Thomas Akehurst * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,6 +16,7 @@ package com.github.tomakehurst.wiremock.common; import static com.github.tomakehurst.wiremock.common.Exceptions.throwUnchecked; +import static com.github.tomakehurst.wiremock.common.Strings.ordinalIndexOf; import static java.nio.charset.StandardCharsets.UTF_8; import com.github.tomakehurst.wiremock.http.QueryParameter; @@ -28,7 +29,6 @@ import java.net.URLDecoder; import java.util.*; import java.util.stream.Collectors; -import org.apache.commons.lang3.StringUtils; public class Urls { @@ -76,7 +76,7 @@ public static String getPath(String url) { } public static String getPathAndQuery(String url) { - return isAbsolute(url) ? url.substring(StringUtils.ordinalIndexOf(url, "/", 3)) : url; + return isAbsolute(url) ? url.substring(ordinalIndexOf(url, "/", 3)) : url; } private static boolean isAbsolute(String url) { diff --git a/src/main/java/com/github/tomakehurst/wiremock/common/filemaker/FilenameMaker.java b/src/main/java/com/github/tomakehurst/wiremock/common/filemaker/FilenameMaker.java index 647dfb5aef..10465724b0 100644 --- a/src/main/java/com/github/tomakehurst/wiremock/common/filemaker/FilenameMaker.java +++ b/src/main/java/com/github/tomakehurst/wiremock/common/filemaker/FilenameMaker.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2023 Thomas Akehurst + * Copyright (C) 2023-2024 Thomas Akehurst * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,6 +16,7 @@ package com.github.tomakehurst.wiremock.common.filemaker; import static com.github.tomakehurst.wiremock.extension.responsetemplating.TemplateEngine.defaultTemplateEngine; +import static java.lang.Math.min; import com.github.tomakehurst.wiremock.extension.responsetemplating.HandlebarsOptimizedTemplate; import com.github.tomakehurst.wiremock.extension.responsetemplating.TemplateEngine; @@ -23,7 +24,6 @@ import java.text.Normalizer; import java.util.Locale; import java.util.regex.Pattern; -import org.apache.commons.lang3.StringUtils; public class FilenameMaker { public static final String DEFAULT_FILENAME_TEMPLATE = @@ -68,7 +68,7 @@ public String sanitizeUrl(String url) { String pathWithoutWhitespace = WHITESPACE.matcher(startingPath).replaceAll("-"); String normalizedPath = Normalizer.normalize(pathWithoutWhitespace, Normalizer.Form.NFD); String slug = sanitise(normalizedPath).replaceAll("^[_]*", "").replaceAll("[_]*$", ""); - slug = StringUtils.truncate(slug, 200); + slug = slug.substring(0, min(slug.length(), 200)); return slug; } diff --git a/src/main/java/com/github/tomakehurst/wiremock/common/url/PathTemplate.java b/src/main/java/com/github/tomakehurst/wiremock/common/url/PathTemplate.java index 1aa655f1b6..7f969eca49 100644 --- a/src/main/java/com/github/tomakehurst/wiremock/common/url/PathTemplate.java +++ b/src/main/java/com/github/tomakehurst/wiremock/common/url/PathTemplate.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2016-2023 Thomas Akehurst + * Copyright (C) 2016-2024 Thomas Akehurst * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -86,7 +86,7 @@ public String render(PathParams pathParams) { } public String withoutVariables() { - return templateString.replaceAll(SPECIAL_SYMBOL_REGEX.pattern(), ""); + return templateString.replaceAll(SPECIAL_SYMBOL_REGEX.pattern(), "_"); } private static String stripFormatCharacters(String parameter) { diff --git a/src/main/java/com/github/tomakehurst/wiremock/core/Options.java b/src/main/java/com/github/tomakehurst/wiremock/core/Options.java index 6611ff96ed..9f3f979e25 100644 --- a/src/main/java/com/github/tomakehurst/wiremock/core/Options.java +++ b/src/main/java/com/github/tomakehurst/wiremock/core/Options.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2013-2023 Thomas Akehurst + * Copyright (C) 2013-2024 Thomas Akehurst * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -52,6 +52,10 @@ enum ChunkedEncodingPolicy { boolean getHttpDisabled(); + boolean getHttp2PlainDisabled(); + + boolean getHttp2TlsDisabled(); + HttpsSettings httpsSettings(); JettySettings jettySettings(); @@ -147,4 +151,8 @@ default int getMaxHttpClientConnections() { Set getTemplatePermittedSystemKeys(); boolean getTemplateEscapingDisabled(); + + default Set getSupportedProxyEncodings() { + return null; + } } diff --git a/src/main/java/com/github/tomakehurst/wiremock/core/ProxyHandler.java b/src/main/java/com/github/tomakehurst/wiremock/core/ProxyHandler.java index 211baac7ab..9b75ce1fb4 100644 --- a/src/main/java/com/github/tomakehurst/wiremock/core/ProxyHandler.java +++ b/src/main/java/com/github/tomakehurst/wiremock/core/ProxyHandler.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2023 Thomas Akehurst + * Copyright (C) 2023-2024 Thomas Akehurst * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -122,6 +122,7 @@ private ResponseDefinition copyResponseDefinition( response.getBodyFileName(), response.getHeaders(), response.getAdditionalProxyRequestHeaders(), + response.getRemoveProxyRequestHeaders(), response.getFixedDelayMilliseconds(), response.getDelayDistribution(), response.getChunkedDribbleDelay(), @@ -145,6 +146,7 @@ private ResponseDefinition copyResponseDefinition( response.getBodyFileName(), response.getHeaders(), response.getAdditionalProxyRequestHeaders(), + response.getRemoveProxyRequestHeaders(), response.getFixedDelayMilliseconds(), response.getDelayDistribution(), response.getChunkedDribbleDelay(), diff --git a/src/main/java/com/github/tomakehurst/wiremock/core/Version.java b/src/main/java/com/github/tomakehurst/wiremock/core/Version.java index 0ff6996620..26da5b793b 100644 --- a/src/main/java/com/github/tomakehurst/wiremock/core/Version.java +++ b/src/main/java/com/github/tomakehurst/wiremock/core/Version.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2023 Thomas Akehurst + * Copyright (C) 2023-2024 Thomas Akehurst * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -22,19 +22,34 @@ import java.util.Properties; public class Version { - private static final Lazy version = lazy(Version::load); + private static final Lazy version = lazy(Version::load); public static String getCurrentVersion() { - return version.get(); + return version.get().version; } - private static String load() { + public static String getGuiVersion() { + return version.get().guiVersion; + } + + private static EnhancedVersion load() { try { Properties properties = new Properties(); - properties.load(Version.class.getResourceAsStream("/version.properties")); - return properties.getProperty("version"); + properties.load(Version.class.getResourceAsStream("/wiremock-gui-version.properties")); + return new EnhancedVersion( + properties.getProperty("version"), properties.getProperty("gui-version")); } catch (NullPointerException | IOException e) { - return "unknown"; + return new EnhancedVersion("unknown", "unknown"); + } + } + + private static class EnhancedVersion { + String version; + String guiVersion; + + public EnhancedVersion(final String version, final String guiVersion) { + this.version = version; + this.guiVersion = guiVersion; } } } diff --git a/src/main/java/com/github/tomakehurst/wiremock/core/WireMockApp.java b/src/main/java/com/github/tomakehurst/wiremock/core/WireMockApp.java index e18d5ff9c3..635a5bf8c5 100644 --- a/src/main/java/com/github/tomakehurst/wiremock/core/WireMockApp.java +++ b/src/main/java/com/github/tomakehurst/wiremock/core/WireMockApp.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2012-2023 Thomas Akehurst + * Copyright (C) 2012-2024 Thomas Akehurst * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -17,91 +17,44 @@ import static com.github.tomakehurst.wiremock.common.ParameterUtils.getFirstNonNull; -import java.util.ArrayList; -import java.util.Collections; -import java.util.HashMap; -import java.util.List; -import java.util.Map; -import java.util.Set; -import java.util.UUID; -import java.util.stream.Collectors; - -import org.apache.commons.lang3.mutable.MutableBoolean; - import com.github.tomakehurst.wiremock.admin.AdminRoutes; import com.github.tomakehurst.wiremock.admin.LimitAndOffsetPaginator; -import com.github.tomakehurst.wiremock.admin.model.GetGlobalSettingsResult; -import com.github.tomakehurst.wiremock.admin.model.GetScenariosResult; -import com.github.tomakehurst.wiremock.admin.model.GetServeEventsResult; -import com.github.tomakehurst.wiremock.admin.model.ListStubMappingsResult; -import com.github.tomakehurst.wiremock.admin.model.ProxyConfig; -import com.github.tomakehurst.wiremock.admin.model.ServeEventQuery; -import com.github.tomakehurst.wiremock.admin.model.SingleServedStubResult; -import com.github.tomakehurst.wiremock.admin.model.SingleStubMappingResult; +import com.github.tomakehurst.wiremock.admin.model.*; import com.github.tomakehurst.wiremock.common.BrowserProxySettings; import com.github.tomakehurst.wiremock.common.FileSource; import com.github.tomakehurst.wiremock.common.xml.Xml; -import com.github.tomakehurst.wiremock.extension.AdminApiExtension; -import com.github.tomakehurst.wiremock.extension.Extensions; -import com.github.tomakehurst.wiremock.extension.GlobalSettingsListener; -import com.github.tomakehurst.wiremock.extension.MappingsLoaderExtension; -import com.github.tomakehurst.wiremock.extension.PostServeAction; -import com.github.tomakehurst.wiremock.extension.ResponseDefinitionTransformer; -import com.github.tomakehurst.wiremock.extension.ResponseDefinitionTransformerV2; -import com.github.tomakehurst.wiremock.extension.ResponseTransformer; -import com.github.tomakehurst.wiremock.extension.ResponseTransformerV2; -import com.github.tomakehurst.wiremock.extension.ServeEventListener; -import com.github.tomakehurst.wiremock.extension.StubLifecycleListener; +import com.github.tomakehurst.wiremock.extension.*; import com.github.tomakehurst.wiremock.extension.requestfilter.RequestFilter; import com.github.tomakehurst.wiremock.extension.requestfilter.RequestFilterV2; import com.github.tomakehurst.wiremock.global.GlobalSettings; import com.github.tomakehurst.wiremock.gui.GuiServeEventListener; -import com.github.tomakehurst.wiremock.http.AdminRequestHandler; -import com.github.tomakehurst.wiremock.http.BasicResponseRenderer; -import com.github.tomakehurst.wiremock.http.ProxyResponseRenderer; -import com.github.tomakehurst.wiremock.http.ResponseDefinition; -import com.github.tomakehurst.wiremock.http.StubRequestHandler; -import com.github.tomakehurst.wiremock.http.StubResponseRenderer; +import com.github.tomakehurst.wiremock.http.*; import com.github.tomakehurst.wiremock.http.client.HttpClient; import com.github.tomakehurst.wiremock.jetty.websockets.Message; import com.github.tomakehurst.wiremock.jetty.websockets.WebSocketEndpoint; import com.github.tomakehurst.wiremock.matching.RequestMatcherExtension; import com.github.tomakehurst.wiremock.matching.RequestPattern; import com.github.tomakehurst.wiremock.matching.StringValuePattern; -import com.github.tomakehurst.wiremock.recording.RecordSpec; -import com.github.tomakehurst.wiremock.recording.RecordSpecBuilder; -import com.github.tomakehurst.wiremock.recording.Recorder; -import com.github.tomakehurst.wiremock.recording.RecordingStatusResult; -import com.github.tomakehurst.wiremock.recording.SnapshotRecordResult; +import com.github.tomakehurst.wiremock.recording.*; import com.github.tomakehurst.wiremock.standalone.MappingsLoader; import com.github.tomakehurst.wiremock.store.DefaultStores; import com.github.tomakehurst.wiremock.store.SettingsStore; import com.github.tomakehurst.wiremock.store.Stores; -import com.github.tomakehurst.wiremock.stubbing.InMemoryScenarios; -import com.github.tomakehurst.wiremock.stubbing.Scenarios; -import com.github.tomakehurst.wiremock.stubbing.ServeEvent; -import com.github.tomakehurst.wiremock.stubbing.StoreBackedStubMappings; -import com.github.tomakehurst.wiremock.stubbing.StubImport; -import com.github.tomakehurst.wiremock.stubbing.StubMapping; -import com.github.tomakehurst.wiremock.stubbing.StubMappings; -import com.github.tomakehurst.wiremock.verification.DisabledRequestJournal; -import com.github.tomakehurst.wiremock.verification.FindNearMissesResult; -import com.github.tomakehurst.wiremock.verification.FindRequestsResult; -import com.github.tomakehurst.wiremock.verification.FindServeEventsResult; -import com.github.tomakehurst.wiremock.verification.LoggedRequest; -import com.github.tomakehurst.wiremock.verification.NearMiss; -import com.github.tomakehurst.wiremock.verification.NearMissCalculator; -import com.github.tomakehurst.wiremock.verification.RequestJournal; -import com.github.tomakehurst.wiremock.verification.RequestJournalDisabledException; -import com.github.tomakehurst.wiremock.verification.StoreBackedRequestJournal; -import com.github.tomakehurst.wiremock.verification.VerificationResult; +import com.github.tomakehurst.wiremock.stubbing.*; +import com.github.tomakehurst.wiremock.verification.*; +import com.jayway.jsonpath.JsonPathException; +import com.jayway.jsonpath.spi.cache.CacheProvider; +import com.jayway.jsonpath.spi.cache.NOOPCache; +import java.util.*; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.stream.Collectors; public class WireMockApp implements StubServer, Admin { public static final String FILES_ROOT = "__files"; public static final String ADMIN_CONTEXT_ROOT = "/__admin"; public static final String MAPPINGS_ROOT = "mappings"; - private static final MutableBoolean FACTORIES_LOADING_OPTIMIZED = new MutableBoolean(false); + private static final AtomicBoolean FACTORIES_LOADING_OPTIMIZED = new AtomicBoolean(false); private final Stores stores; private final Scenarios scenarios; @@ -125,9 +78,19 @@ public class WireMockApp implements StubServer, Admin { public WireMockApp(Options options, Container container) { this.proxyHandler = new ProxyHandler(this); - if (!options.getDisableOptimizeXmlFactoriesLoading() && FACTORIES_LOADING_OPTIMIZED.isFalse()) { + if (!options.getDisableOptimizeXmlFactoriesLoading() + && Boolean.FALSE.equals(FACTORIES_LOADING_OPTIMIZED.get())) { Xml.optimizeFactoriesLoading(); - FACTORIES_LOADING_OPTIMIZED.setTrue(); + FACTORIES_LOADING_OPTIMIZED.set(true); + } + + try { + // Disabling JsonPath's cache due to + // https://github.com/json-path/JsonPath/issues/975#issuecomment-1867293053 and the fact that + // we're now doing our own caching. + CacheProvider.setCache(new NOOPCache()); + } catch (JsonPathException ignored) { + // May fail on subsequent runs, but this doesn't matter } this.options = options; @@ -141,22 +104,38 @@ public WireMockApp(Options options, Container container) { this.settingsStore = stores.getSettingsStore(); extensions = - new Extensions(options.getDeclaredExtensions(), this, options, stores, options.filesRoot().child(FILES_ROOT)); + new Extensions( + options.getDeclaredExtensions(), + this, + options, + stores, + options.filesRoot().child(FILES_ROOT)); extensions.load(); - Map customMatchers = extensions.ofType(RequestMatcherExtension.class); + Map customMatchers = + extensions.ofType(RequestMatcherExtension.class); - requestJournal = options.requestJournalDisabled() ? - new DisabledRequestJournal() : - new StoreBackedRequestJournal(options.maxRequestJournalEntries().orElse(null), customMatchers, - stores.getRequestJournalStore()); + requestJournal = + options.requestJournalDisabled() + ? new DisabledRequestJournal() + : new StoreBackedRequestJournal( + options.maxRequestJournalEntries().orElse(null), + customMatchers, + stores.getRequestJournalStore()); scenarios = new InMemoryScenarios(stores.getScenariosStore()); - stubMappings = new StoreBackedStubMappings(stores.getStubStore(), scenarios, customMatchers, - extensions.ofType(ResponseDefinitionTransformer.class), extensions.ofType(ResponseDefinitionTransformerV2.class), - stores.getFilesBlobStore(), List.copyOf(extensions.ofType(StubLifecycleListener.class).values())); + stubMappings = + new StoreBackedStubMappings( + stores.getStubStore(), + scenarios, + customMatchers, + extensions.ofType(ResponseDefinitionTransformer.class), + extensions.ofType(ResponseDefinitionTransformerV2.class), + stores.getFilesBlobStore(), + List.copyOf(extensions.ofType(StubLifecycleListener.class).values())); nearMissCalculator = new NearMissCalculator(stubMappings, requestJournal, scenarios); - recorder = new Recorder(this, extensions, stores.getFilesBlobStore(), stores.getRecorderStateStore()); + recorder = + new Recorder(this, extensions, stores.getFilesBlobStore(), stores.getRecorderStateStore()); globalSettingsListeners = List.copyOf(extensions.ofType(GlobalSettingsListener.class).values()); this.mappingsLoaderExtensions = extensions.ofType(MappingsLoaderExtension.class); @@ -164,12 +143,18 @@ public WireMockApp(Options options, Container container) { loadDefaultMappings(); } - public WireMockApp(boolean browserProxyingEnabled, MappingsLoader defaultMappingsLoader, - Map mappingsLoaderExtensions, MappingsSaver mappingsSaver, - boolean requestJournalDisabled, Integer maxRequestJournalEntries, - Map transformers, - Map v2transformers, Map requestMatchers, - FileSource rootFileSource, Container container) { + public WireMockApp( + boolean browserProxyingEnabled, + MappingsLoader defaultMappingsLoader, + Map mappingsLoaderExtensions, + MappingsSaver mappingsSaver, + boolean requestJournalDisabled, + Integer maxRequestJournalEntries, + Map transformers, + Map v2transformers, + Map requestMatchers, + FileSource rootFileSource, + Container container) { this.proxyHandler = new ProxyHandler(this); @@ -180,77 +165,122 @@ public WireMockApp(boolean browserProxyingEnabled, MappingsLoader defaultMapping this.mappingsLoaderExtensions = mappingsLoaderExtensions; this.mappingsSaver = mappingsSaver; this.settingsStore = stores.getSettingsStore(); - requestJournal = requestJournalDisabled ? - new DisabledRequestJournal() : - new StoreBackedRequestJournal(maxRequestJournalEntries, requestMatchers, stores.getRequestJournalStore()); + requestJournal = + requestJournalDisabled + ? new DisabledRequestJournal() + : new StoreBackedRequestJournal( + maxRequestJournalEntries, requestMatchers, stores.getRequestJournalStore()); scenarios = new InMemoryScenarios(stores.getScenariosStore()); stubMappings = - new StoreBackedStubMappings(stores.getStubStore(), scenarios, requestMatchers, transformers, v2transformers, - stores.getFilesBlobStore(), Collections.emptyList()); + new StoreBackedStubMappings( + stores.getStubStore(), + scenarios, + requestMatchers, + transformers, + v2transformers, + stores.getFilesBlobStore(), + Collections.emptyList()); this.container = container; nearMissCalculator = new NearMissCalculator(stubMappings, requestJournal, scenarios); - recorder = new Recorder(this, extensions, stores.getFilesBlobStore(), stores.getRecorderStateStore()); + recorder = + new Recorder(this, extensions, stores.getFilesBlobStore(), stores.getRecorderStateStore()); globalSettingsListeners = Collections.emptyList(); loadDefaultMappings(); } public AdminRequestHandler buildAdminRequestHandler() { - AdminRoutes adminRoutes = AdminRoutes.forServer(extensions.ofType(AdminApiExtension.class).values(), stores); - return new AdminRequestHandler(adminRoutes, this, new BasicResponseRenderer(), options.getAdminAuthenticator(), - options.getHttpsRequiredForAdminApi(), getAdminRequestFilters(), getV2AdminRequestFilters(), - options.getDataTruncationSettings()); + AdminRoutes adminRoutes = + AdminRoutes.forServer(extensions.ofType(AdminApiExtension.class).values(), stores); + return new AdminRequestHandler( + adminRoutes, + this, + new BasicResponseRenderer(), + options.getAdminAuthenticator(), + options.getHttpsRequiredForAdminApi(), + getAdminRequestFilters(), + getV2AdminRequestFilters(), + options.getDataTruncationSettings()); } public StubRequestHandler buildStubRequestHandler() { Map postServeActions = extensions.ofType(PostServeAction.class); - Map concatenatedMap = new HashMap<>(extensions.ofType(ServeEventListener.class)); + Map concatenatedMap = + new HashMap<>(extensions.ofType(ServeEventListener.class)); concatenatedMap.put("wiremock-gui", new GuiServeEventListener()); - Map serveEventListeners = Collections.unmodifiableMap(concatenatedMap); + Map serveEventListeners = + Collections.unmodifiableMap(concatenatedMap); BrowserProxySettings browserProxySettings = options.browserProxySettings(); final com.github.tomakehurst.wiremock.http.client.HttpClientFactory httpClientFactory = - extensions.ofType(com.github.tomakehurst.wiremock.http.client.HttpClientFactory.class).values().stream() - .findFirst().orElse(options.httpClientFactory()); + extensions + .ofType(com.github.tomakehurst.wiremock.http.client.HttpClientFactory.class) + .values() + .stream() + .findFirst() + .orElse(options.httpClientFactory()); final HttpClient reverseProxyClient = - httpClientFactory.buildHttpClient(options, true, Collections.emptyList(), true); + httpClientFactory.buildHttpClient(options, true, Collections.emptyList(), true); final HttpClient forwardProxyClient = - httpClientFactory.buildHttpClient(options, browserProxySettings.trustAllProxyTargets(), - browserProxySettings.trustAllProxyTargets() ? - Collections.emptyList() : - browserProxySettings.trustedProxyTargets(), false); - - return new StubRequestHandler(this, new StubResponseRenderer(options.getStores().getFilesBlobStore(), settingsStore, - new ProxyResponseRenderer(options.shouldPreserveHostHeader(), options.proxyHostHeader(), settingsStore, - options.getStubCorsEnabled(), reverseProxyClient, forwardProxyClient), - List.copyOf(extensions.ofType(ResponseTransformer.class).values()), - List.copyOf(extensions.ofType(ResponseTransformerV2.class).values())), this, postServeActions, - serveEventListeners, requestJournal, getStubRequestFilters(), getV2StubRequestFilters(), - options.getStubRequestLoggingDisabled(), options.getDataTruncationSettings(), - options.getNotMatchedRendererFactory().apply(extensions)); + httpClientFactory.buildHttpClient( + options, + browserProxySettings.trustAllProxyTargets(), + browserProxySettings.trustAllProxyTargets() + ? Collections.emptyList() + : browserProxySettings.trustedProxyTargets(), + false); + + return new StubRequestHandler( + this, + new StubResponseRenderer( + options.getStores().getFilesBlobStore(), + settingsStore, + new ProxyResponseRenderer( + options.shouldPreserveHostHeader(), + options.proxyHostHeader(), + settingsStore, + options.getStubCorsEnabled(), + options.getSupportedProxyEncodings(), + reverseProxyClient, + forwardProxyClient), + List.copyOf(extensions.ofType(ResponseTransformer.class).values()), + List.copyOf(extensions.ofType(ResponseTransformerV2.class).values())), + this, + postServeActions, + serveEventListeners, + requestJournal, + getStubRequestFilters(), + getV2StubRequestFilters(), + options.getStubRequestLoggingDisabled(), + options.getDataTruncationSettings(), + options.getNotMatchedRendererFactory().apply(extensions)); } private List getAdminRequestFilters() { - return extensions.ofType(RequestFilter.class).values().stream().filter(RequestFilter::applyToAdmin) - .collect(Collectors.toList()); + return extensions.ofType(RequestFilter.class).values().stream() + .filter(RequestFilter::applyToAdmin) + .collect(Collectors.toList()); } private List getV2AdminRequestFilters() { - return extensions.ofType(RequestFilterV2.class).values().stream().filter(RequestFilterV2::applyToAdmin) - .collect(Collectors.toList()); + return extensions.ofType(RequestFilterV2.class).values().stream() + .filter(RequestFilterV2::applyToAdmin) + .collect(Collectors.toList()); } private List getStubRequestFilters() { - return extensions.ofType(RequestFilter.class).values().stream().filter(RequestFilter::applyToStubs) - .collect(Collectors.toList()); + return extensions.ofType(RequestFilter.class).values().stream() + .filter(RequestFilter::applyToStubs) + .collect(Collectors.toList()); } private List getV2StubRequestFilters() { - return extensions.ofType(RequestFilterV2.class).values().stream().filter(RequestFilterV2::applyToStubs) - .collect(Collectors.toList()); + return extensions.ofType(RequestFilterV2.class).values().stream() + .filter(RequestFilterV2::applyToStubs) + .collect(Collectors.toList()); } private void loadDefaultMappings() { @@ -268,9 +298,12 @@ public void loadMappingsUsing(final MappingsLoader mappingsLoader) { public ServeEvent serveStubFor(ServeEvent initialServeEvent) { ServeEvent serveEvent = stubMappings.serveFor(initialServeEvent); - if (serveEvent.isNoExactMatch() && browserProxyingEnabled && serveEvent.getRequest().isBrowserProxyRequest() - && getGlobalSettings().getSettings().getProxyPassThrough()) { - return ServeEvent.ofUnmatched(serveEvent.getRequest(), ResponseDefinition.browserProxy(serveEvent.getRequest())); + if (serveEvent.isNoExactMatch() + && browserProxyingEnabled + && serveEvent.getRequest().isBrowserProxyRequest() + && getGlobalSettings().getSettings().getProxyPassThrough()) { + return ServeEvent.ofUnmatched( + serveEvent.getRequest(), ResponseDefinition.browserProxy(serveEvent.getRequest())); } return serveEvent; @@ -292,11 +325,14 @@ public void addStubMapping(StubMapping stubMapping) { @Override public void removeStubMapping(StubMapping stubMapping) { - stubMappings.get(stubMapping.getId()).ifPresent(stubToDelete -> { - if (stubToDelete.shouldBePersisted()) { - mappingsSaver.remove(stubToDelete); - } - }); + stubMappings + .get(stubMapping.getId()) + .ifPresent( + stubToDelete -> { + if (stubToDelete.shouldBePersisted()) { + mappingsSaver.remove(stubToDelete); + } + }); stubMappings.removeMapping(stubMapping); @@ -391,7 +427,7 @@ public GetServeEventsResult getServeEvents(ServeEventQuery query) { return GetServeEventsResult.requestJournalEnabled(LimitAndOffsetPaginator.none(serveEvents)); } catch (RequestJournalDisabledException e) { return GetServeEventsResult.requestJournalDisabled( - LimitAndOffsetPaginator.none(requestJournal.getAllServeEvents())); + LimitAndOffsetPaginator.none(requestJournal.getAllServeEvents())); } } @@ -423,8 +459,10 @@ public FindRequestsResult findRequestsMatching(RequestPattern requestPattern) { public FindRequestsResult findUnmatchedRequests() { try { List requests = - requestJournal.getAllServeEvents().stream().filter(ServeEvent::isNoExactMatch).map(ServeEvent::getRequest) - .collect(Collectors.toList()); + requestJournal.getAllServeEvents().stream() + .filter(ServeEvent::isNoExactMatch) + .map(ServeEvent::getRequest) + .collect(Collectors.toList()); return FindRequestsResult.withRequests(requests); } catch (RequestJournalDisabledException e) { return FindRequestsResult.withRequestJournalDisabled(); @@ -442,15 +480,19 @@ public FindServeEventsResult removeServeEventsMatching(RequestPattern requestPat } @Override - public FindServeEventsResult removeServeEventsForStubsMatchingMetadata(StringValuePattern metadataPattern) { - return new FindServeEventsResult(requestJournal.removeServeEventsForStubsMatchingMetadata(metadataPattern)); + public FindServeEventsResult removeServeEventsForStubsMatchingMetadata( + StringValuePattern metadataPattern) { + return new FindServeEventsResult( + requestJournal.removeServeEventsForStubsMatchingMetadata(metadataPattern)); } @Override public FindNearMissesResult findNearMissesForUnmatchedRequests() { List nearMisses = new ArrayList<>(); List unmatchedServeEvents = - requestJournal.getAllServeEvents().stream().filter(ServeEvent::isNoExactMatch).collect(Collectors.toList()); + requestJournal.getAllServeEvents().stream() + .filter(ServeEvent::isNoExactMatch) + .collect(Collectors.toList()); for (ServeEvent serveEvent : unmatchedServeEvents) { nearMisses.addAll(nearMissCalculator.findNearestTo(serveEvent.getRequest())); @@ -590,7 +632,8 @@ public RecordingStatusResult getRecordingStatus() { @Override public ListStubMappingsResult findAllStubsByMetadata(StringValuePattern pattern) { - return new ListStubMappingsResult(LimitAndOffsetPaginator.none(stubMappings.findByMetadata(pattern))); + return new ListStubMappingsResult( + LimitAndOffsetPaginator.none(stubMappings.findByMetadata(pattern))); } @Override @@ -604,7 +647,8 @@ public void removeStubsByMetadata(StringValuePattern pattern) { @Override public void importStubs(StubImport stubImport) { List mappings = stubImport.getMappings(); - StubImport.Options importOptions = getFirstNonNull(stubImport.getImportOptions(), StubImport.Options.DEFAULTS); + StubImport.Options importOptions = + getFirstNonNull(stubImport.getImportOptions(), StubImport.Options.DEFAULTS); for (int i = mappings.size() - 1; i >= 0; i--) { StubMapping mapping = mappings.get(i); diff --git a/src/main/java/com/github/tomakehurst/wiremock/core/WireMockConfiguration.java b/src/main/java/com/github/tomakehurst/wiremock/core/WireMockConfiguration.java index 26b743a955..205a3f71da 100644 --- a/src/main/java/com/github/tomakehurst/wiremock/core/WireMockConfiguration.java +++ b/src/main/java/com/github/tomakehurst/wiremock/core/WireMockConfiguration.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2013-2023 Thomas Akehurst + * Copyright (C) 2013-2024 Thomas Akehurst * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -65,6 +65,8 @@ public class WireMockConfiguration implements Options { private boolean disableOptimizeXmlFactoriesLoading = false; private int portNumber = DEFAULT_PORT; private boolean httpDisabled = false; + private boolean http2PlainDisabled = false; + private boolean http2TlsDisabled = false; private String bindAddress = DEFAULT_BIND_ADDRESS; private int containerThreads = DEFAULT_CONTAINER_THREADS; @@ -144,6 +146,8 @@ public class WireMockConfiguration implements Options { private Long maxTemplateCacheEntries = null; private boolean templateEscapingDisabled = true; + private Set supportedProxyEncodings = null; + private MappingsSource getMappingsSource() { if (mappingsSource == null) { mappingsSource = @@ -194,6 +198,16 @@ public WireMockConfiguration httpDisabled(boolean httpDisabled) { return this; } + public WireMockConfiguration http2PlainDisabled(boolean enabled) { + this.http2PlainDisabled = enabled; + return this; + } + + public WireMockConfiguration http2TlsDisabled(boolean enabled) { + this.http2TlsDisabled = enabled; + return this; + } + public WireMockConfiguration httpsPort(Integer httpsPort) { this.httpsPort = httpsPort; return this; @@ -551,6 +565,15 @@ public WireMockConfiguration withMaxTemplateCacheEntries(Long maxTemplateCacheEn return this; } + public WireMockConfiguration withSupportedProxyEncodings(Set supportedProxyEncodings) { + this.supportedProxyEncodings = supportedProxyEncodings; + return this; + } + + public WireMockConfiguration withSupportedProxyEncodings(String... supportedProxyEncodings) { + return withSupportedProxyEncodings(Set.of(supportedProxyEncodings)); + } + @Override public int portNumber() { return portNumber; @@ -561,6 +584,16 @@ public boolean getHttpDisabled() { return httpDisabled; } + @Override + public boolean getHttp2PlainDisabled() { + return http2PlainDisabled; + } + + @Override + public boolean getHttp2TlsDisabled() { + return http2TlsDisabled; + } + @Override public int containerThreads() { return containerThreads; @@ -815,4 +848,9 @@ public Set getTemplatePermittedSystemKeys() { public boolean getTemplateEscapingDisabled() { return templateEscapingDisabled; } + + @Override + public Set getSupportedProxyEncodings() { + return supportedProxyEncodings; + } } diff --git a/src/main/java/com/github/tomakehurst/wiremock/extension/ExtensionDeclarations.java b/src/main/java/com/github/tomakehurst/wiremock/extension/ExtensionDeclarations.java index b9fb53eeae..66ac8226fc 100644 --- a/src/main/java/com/github/tomakehurst/wiremock/extension/ExtensionDeclarations.java +++ b/src/main/java/com/github/tomakehurst/wiremock/extension/ExtensionDeclarations.java @@ -15,7 +15,6 @@ */ package com.github.tomakehurst.wiremock.extension; -import static com.github.tomakehurst.wiremock.common.LocalNotifier.notifier; import static java.util.Arrays.asList; import java.util.*; @@ -77,7 +76,7 @@ public List getFactories() { private boolean removeWebhook(String className) { if (className.equals(Webhooks.class.getName())) { - notifier().info(WEBHOOK_MESSAGE); + System.out.println(WEBHOOK_MESSAGE); return false; } return true; diff --git a/src/main/java/com/github/tomakehurst/wiremock/extension/requestfilter/RequestWrapper.java b/src/main/java/com/github/tomakehurst/wiremock/extension/requestfilter/RequestWrapper.java index 06d2f26ed1..d9e40ddc82 100644 --- a/src/main/java/com/github/tomakehurst/wiremock/extension/requestfilter/RequestWrapper.java +++ b/src/main/java/com/github/tomakehurst/wiremock/extension/requestfilter/RequestWrapper.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2018-2023 Thomas Akehurst + * Copyright (C) 2018-2024 Thomas Akehurst * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -17,8 +17,8 @@ import static com.github.tomakehurst.wiremock.common.Encoding.encodeBase64; import static com.github.tomakehurst.wiremock.common.ParameterUtils.getFirstNonNull; -import static org.apache.commons.lang3.StringUtils.countMatches; -import static org.apache.commons.lang3.StringUtils.ordinalIndexOf; +import static com.github.tomakehurst.wiremock.common.Strings.countMatches; +import static com.github.tomakehurst.wiremock.common.Strings.ordinalIndexOf; import com.github.tomakehurst.wiremock.http.*; import java.util.*; @@ -88,7 +88,7 @@ public static Builder create() { public String getUrl() { String absoluteUrl = getAbsoluteUrl(); int relativeStartIndex = - countMatches(absoluteUrl, "/") >= 3 + countMatches(absoluteUrl, '/') >= 3 ? ordinalIndexOf(absoluteUrl, "/", 3) : absoluteUrl.length(); return absoluteUrl.substring(relativeStartIndex); diff --git a/src/main/java/com/github/tomakehurst/wiremock/extension/responsetemplating/HandlebarsOptimizedTemplate.java b/src/main/java/com/github/tomakehurst/wiremock/extension/responsetemplating/HandlebarsOptimizedTemplate.java index c48674636e..8f5e908b40 100644 --- a/src/main/java/com/github/tomakehurst/wiremock/extension/responsetemplating/HandlebarsOptimizedTemplate.java +++ b/src/main/java/com/github/tomakehurst/wiremock/extension/responsetemplating/HandlebarsOptimizedTemplate.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2019-2023 Thomas Akehurst + * Copyright (C) 2019-2024 Thomas Akehurst * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -19,6 +19,7 @@ import com.github.jknack.handlebars.Handlebars; import com.github.jknack.handlebars.Template; import com.github.tomakehurst.wiremock.common.Exceptions; +import com.github.tomakehurst.wiremock.common.RequestCache; import java.io.IOException; import java.io.Writer; import org.apache.commons.io.output.StringBuilderWriter; @@ -62,8 +63,8 @@ private static Template uncheckedCompileTemplate(Handlebars handlebars, String t } public String apply(Object contextData) { - final RenderCache renderCache = new RenderCache(); - Context context = Context.newBuilder(contextData).combine("renderCache", renderCache).build(); + final RequestCache requestCache = RequestCache.getCurrent(); + Context context = Context.newBuilder(contextData).combine("requestCache", requestCache).build(); return startContent + applyTemplate(context) + endContent; } diff --git a/src/main/java/com/github/tomakehurst/wiremock/extension/responsetemplating/ResponseTemplateTransformer.java b/src/main/java/com/github/tomakehurst/wiremock/extension/responsetemplating/ResponseTemplateTransformer.java index 2d24bd0267..94a83f49f3 100644 --- a/src/main/java/com/github/tomakehurst/wiremock/extension/responsetemplating/ResponseTemplateTransformer.java +++ b/src/main/java/com/github/tomakehurst/wiremock/extension/responsetemplating/ResponseTemplateTransformer.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2016-2023 Thomas Akehurst + * Copyright (C) 2016-2024 Thomas Akehurst * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -172,6 +172,13 @@ public ResponseDefinition transform(ServeEvent serveEvent) { key, proxyHttpHeaders.getHeader(key).firstValue()); } } + + if (responseDefinition.getRemoveProxyRequestHeaders() != null) { + for (String key : responseDefinition.getRemoveProxyRequestHeaders()) { + newProxyResponseDefBuilder.withRemoveRequestHeader(key); + } + } + return newProxyResponseDefBuilder.build(); } else { return newResponseDefBuilder.build(); diff --git a/src/main/java/com/github/tomakehurst/wiremock/extension/responsetemplating/TemplatedUrlPath.java b/src/main/java/com/github/tomakehurst/wiremock/extension/responsetemplating/TemplatedUrlPath.java index 40aeaf05a9..990e64b6d0 100644 --- a/src/main/java/com/github/tomakehurst/wiremock/extension/responsetemplating/TemplatedUrlPath.java +++ b/src/main/java/com/github/tomakehurst/wiremock/extension/responsetemplating/TemplatedUrlPath.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2023 Thomas Akehurst + * Copyright (C) 2023-2024 Thomas Akehurst * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -15,12 +15,13 @@ */ package com.github.tomakehurst.wiremock.extension.responsetemplating; +import static com.github.tomakehurst.wiremock.common.Strings.isNotEmpty; + import com.github.tomakehurst.wiremock.common.Urls; import com.github.tomakehurst.wiremock.common.url.PathTemplate; import java.util.Iterator; import java.util.LinkedHashMap; import java.util.List; -import org.apache.commons.lang3.StringUtils; public class TemplatedUrlPath extends LinkedHashMap implements Iterable { @@ -36,7 +37,7 @@ private void addAllPathSegments() { final List pathSegments = Urls.getPathSegments(originalPath); int i = 0; for (String pathNode : pathSegments) { - if (StringUtils.isNotEmpty(pathNode)) { + if (isNotEmpty(pathNode)) { String key = String.valueOf(i++); put(key, pathNode); } diff --git a/src/main/java/com/github/tomakehurst/wiremock/extension/responsetemplating/UrlPath.java b/src/main/java/com/github/tomakehurst/wiremock/extension/responsetemplating/UrlPath.java index 32dab33835..7dd61e4236 100644 --- a/src/main/java/com/github/tomakehurst/wiremock/extension/responsetemplating/UrlPath.java +++ b/src/main/java/com/github/tomakehurst/wiremock/extension/responsetemplating/UrlPath.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2016-2023 Thomas Akehurst + * Copyright (C) 2016-2024 Thomas Akehurst * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -15,10 +15,11 @@ */ package com.github.tomakehurst.wiremock.extension.responsetemplating; +import static com.github.tomakehurst.wiremock.common.Strings.isNotEmpty; + import com.github.tomakehurst.wiremock.common.Urls; import java.net.URI; import java.util.ArrayList; -import org.apache.commons.lang3.StringUtils; public class UrlPath extends ArrayList { @@ -29,7 +30,7 @@ public UrlPath(String url) { Urls.getPathSegments(originalPath) .forEach( pathNode -> { - if (StringUtils.isNotEmpty(pathNode)) { + if (isNotEmpty(pathNode)) { add(pathNode); } }); diff --git a/src/main/java/com/github/tomakehurst/wiremock/extension/responsetemplating/helpers/HandlebarsHelper.java b/src/main/java/com/github/tomakehurst/wiremock/extension/responsetemplating/helpers/HandlebarsHelper.java index 14870d65f7..66a8979498 100644 --- a/src/main/java/com/github/tomakehurst/wiremock/extension/responsetemplating/helpers/HandlebarsHelper.java +++ b/src/main/java/com/github/tomakehurst/wiremock/extension/responsetemplating/helpers/HandlebarsHelper.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2017-2021 Thomas Akehurst + * Copyright (C) 2017-2024 Thomas Akehurst * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -19,7 +19,7 @@ import com.github.jknack.handlebars.Helper; import com.github.jknack.handlebars.Options; -import com.github.tomakehurst.wiremock.extension.responsetemplating.RenderCache; +import com.github.tomakehurst.wiremock.common.RequestCache; /** * This abstract class is the base for all defined Handlebars helper in wiremock. It basically @@ -74,7 +74,7 @@ private String formatMessage(String message) { return ERROR_PREFIX + message + ERROR_SUFFIX; } - protected static RenderCache getRenderCache(Options options) { - return options.get("renderCache", new RenderCache()); + protected static RequestCache getRequestCache(Options options) { + return options.get("requestCache", new RequestCache()); } } diff --git a/src/main/java/com/github/tomakehurst/wiremock/extension/responsetemplating/helpers/HandlebarsJsonPathHelper.java b/src/main/java/com/github/tomakehurst/wiremock/extension/responsetemplating/helpers/HandlebarsJsonPathHelper.java index f4dd42d776..8a73eda78b 100644 --- a/src/main/java/com/github/tomakehurst/wiremock/extension/responsetemplating/helpers/HandlebarsJsonPathHelper.java +++ b/src/main/java/com/github/tomakehurst/wiremock/extension/responsetemplating/helpers/HandlebarsJsonPathHelper.java @@ -18,7 +18,7 @@ import static com.github.tomakehurst.wiremock.common.ParameterUtils.getFirstNonNull; import com.github.jknack.handlebars.Options; -import com.github.tomakehurst.wiremock.extension.responsetemplating.RenderCache; +import com.github.tomakehurst.wiremock.common.RequestCache; import com.jayway.jsonpath.Configuration; import com.jayway.jsonpath.DocumentContext; import com.jayway.jsonpath.InvalidJsonException; @@ -60,9 +60,9 @@ public Object apply(final Object input, final Options options) throws IOExceptio } private Object getValue(JsonPath jsonPath, DocumentContext jsonDocument, Options options) { - RenderCache renderCache = getRenderCache(options); - RenderCache.Key cacheKey = RenderCache.Key.keyFor(Object.class, jsonPath, jsonDocument); - Object value = renderCache.get(cacheKey); + RequestCache requestCache = getRequestCache(options); + RequestCache.Key cacheKey = RequestCache.Key.keyFor(Object.class, jsonPath, jsonDocument); + Object value = requestCache.get(cacheKey); if (value == null) { Object defaultValue = options.hash != null ? options.hash("default") : null; try { @@ -75,20 +75,20 @@ private Object getValue(JsonPath jsonPath, DocumentContext jsonDocument, Options value = getFirstNonNull(defaultValue, ""); } - renderCache.put(cacheKey, value); + requestCache.put(cacheKey, value); } return value; } private DocumentContext getJsonDocument(Object json, Options options) { - RenderCache renderCache = getRenderCache(options); - RenderCache.Key cacheKey = RenderCache.Key.keyFor(DocumentContext.class, json); - DocumentContext document = renderCache.get(cacheKey); + RequestCache requestCache = getRequestCache(options); + RequestCache.Key cacheKey = RequestCache.Key.keyFor(DocumentContext.class, json); + DocumentContext document = requestCache.get(cacheKey); if (document == null) { document = json instanceof String ? parseContext.parse((String) json) : parseContext.parse(json); - renderCache.put(cacheKey, document); + requestCache.put(cacheKey, document); } return document; diff --git a/src/main/java/com/github/tomakehurst/wiremock/extension/responsetemplating/helpers/HandlebarsRandomValuesHelper.java b/src/main/java/com/github/tomakehurst/wiremock/extension/responsetemplating/helpers/HandlebarsRandomValuesHelper.java index 6ad06927db..114eb779d7 100644 --- a/src/main/java/com/github/tomakehurst/wiremock/extension/responsetemplating/helpers/HandlebarsRandomValuesHelper.java +++ b/src/main/java/com/github/tomakehurst/wiremock/extension/responsetemplating/helpers/HandlebarsRandomValuesHelper.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2018-2021 Thomas Akehurst + * Copyright (C) 2018-2024 Thomas Akehurst * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -15,10 +15,11 @@ */ package com.github.tomakehurst.wiremock.extension.responsetemplating.helpers; +import static com.github.tomakehurst.wiremock.common.Strings.*; + import com.github.jknack.handlebars.Options; import java.io.IOException; import java.util.UUID; -import org.apache.commons.lang3.RandomStringUtils; public class HandlebarsRandomValuesHelper extends HandlebarsHelper { @@ -32,25 +33,25 @@ public Object apply(Void context, Options options) throws IOException { switch (type) { case "ALPHANUMERIC": - rawValue = RandomStringUtils.randomAlphanumeric(length); + rawValue = randomAlphanumeric(length); break; case "ALPHABETIC": - rawValue = RandomStringUtils.randomAlphabetic(length); + rawValue = randomAlphabetic(length); break; case "NUMERIC": - rawValue = RandomStringUtils.randomNumeric(length); + rawValue = randomNumeric(length); break; case "ALPHANUMERIC_AND_SYMBOLS": - rawValue = RandomStringUtils.random(length, 33, 126, false, false); + rawValue = random(length, 33, 126, false, false); break; case "UUID": rawValue = UUID.randomUUID().toString(); break; case "HEXADECIMAL": - rawValue = RandomStringUtils.random(length, "ABCDEF0123456789"); + rawValue = random(length, "ABCDEF0123456789"); break; default: - rawValue = RandomStringUtils.randomAscii(length); + rawValue = randomAscii(length); break; } return uppercase ? rawValue.toUpperCase() : rawValue.toLowerCase(); diff --git a/src/main/java/com/github/tomakehurst/wiremock/extension/responsetemplating/helpers/HandlebarsXPathHelper.java b/src/main/java/com/github/tomakehurst/wiremock/extension/responsetemplating/helpers/HandlebarsXPathHelper.java index e9c87f1681..ffe9ec27f5 100644 --- a/src/main/java/com/github/tomakehurst/wiremock/extension/responsetemplating/helpers/HandlebarsXPathHelper.java +++ b/src/main/java/com/github/tomakehurst/wiremock/extension/responsetemplating/helpers/HandlebarsXPathHelper.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2017-2021 Thomas Akehurst + * Copyright (C) 2017-2024 Thomas Akehurst * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -17,8 +17,8 @@ import com.github.jknack.handlebars.Options; import com.github.tomakehurst.wiremock.common.ListOrSingle; +import com.github.tomakehurst.wiremock.common.RequestCache; import com.github.tomakehurst.wiremock.common.xml.*; -import com.github.tomakehurst.wiremock.extension.responsetemplating.RenderCache; import java.io.IOException; /** @@ -62,25 +62,25 @@ public Object apply(final String inputXml, final Options options) throws IOExcep private ListOrSingle getXmlNodes( String xPathExpression, XmlDocument doc, Options options) { - RenderCache renderCache = getRenderCache(options); - RenderCache.Key cacheKey = RenderCache.Key.keyFor(XmlDocument.class, xPathExpression, doc); - ListOrSingle nodes = renderCache.get(cacheKey); + RequestCache requestCache = getRequestCache(options); + RequestCache.Key cacheKey = RequestCache.Key.keyFor(XmlDocument.class, xPathExpression, doc); + ListOrSingle nodes = requestCache.get(cacheKey); if (nodes == null) { nodes = doc.findNodes(xPathExpression); - renderCache.put(cacheKey, nodes); + requestCache.put(cacheKey, nodes); } return nodes; } private XmlDocument getXmlDocument(String xml, Options options) { - RenderCache renderCache = getRenderCache(options); - RenderCache.Key cacheKey = RenderCache.Key.keyFor(XmlDocument.class, xml); - XmlDocument document = renderCache.get(cacheKey); + RequestCache requestCache = getRequestCache(options); + RequestCache.Key cacheKey = RequestCache.Key.keyFor(XmlDocument.class, xml); + XmlDocument document = requestCache.get(cacheKey); if (document == null) { document = Xml.parse(xml); - renderCache.put(cacheKey, document); + requestCache.put(cacheKey, document); } return document; diff --git a/src/main/java/com/github/tomakehurst/wiremock/extension/responsetemplating/helpers/JWTHelper.java b/src/main/java/com/github/tomakehurst/wiremock/extension/responsetemplating/helpers/JWTHelper.java index 832e550636..09ddd3af5f 100644 --- a/src/main/java/com/github/tomakehurst/wiremock/extension/responsetemplating/helpers/JWTHelper.java +++ b/src/main/java/com/github/tomakehurst/wiremock/extension/responsetemplating/helpers/JWTHelper.java @@ -1,3 +1,18 @@ +/* + * Copyright (C) 2024 Thomas Akehurst + * + * 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 + * + * http://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. + */ package com.github.tomakehurst.wiremock.extension.responsetemplating.helpers; import com.fasterxml.jackson.core.type.TypeReference; @@ -6,10 +21,6 @@ import io.jsonwebtoken.JwtBuilder; import io.jsonwebtoken.Jwts; import io.jsonwebtoken.SignatureAlgorithm; -import org.apache.commons.lang3.StringUtils; -import org.bouncycastle.jce.provider.BouncyCastleProvider; - -import javax.crypto.spec.SecretKeySpec; import java.io.IOException; import java.security.Key; import java.security.KeyFactory; @@ -19,88 +30,93 @@ import java.util.Base64; import java.util.HashMap; import java.util.Map; +import javax.crypto.spec.SecretKeySpec; +import org.bouncycastle.jce.provider.BouncyCastleProvider; public class JWTHelper extends HandlebarsHelper { - private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper(); - - @Override - public Object apply(Object context, Options options) throws IOException { - String signAlgo = options.hash("algo", SignatureAlgorithm.RS256.name()); - String key = options.hash("key", null); - String claims = options.hash("claims", null); - String payload = options.hash("payload", null); - String header = options.hash("header", null); - - try { - return createJWT(SignatureAlgorithm.valueOf(signAlgo), key, jsonToMap(claims), payload, jsonToMap(header)); - } catch (Exception e) { - return this.handleError(e.getMessage(), e); - } + private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper(); + + @Override + public Object apply(Object context, Options options) throws IOException { + String signAlgo = options.hash("algo", SignatureAlgorithm.RS256.name()); + String key = options.hash("key", null); + String claims = options.hash("claims", null); + String payload = options.hash("payload", null); + String header = options.hash("header", null); + + try { + return createJWT( + SignatureAlgorithm.valueOf(signAlgo), key, jsonToMap(claims), payload, jsonToMap(header)); + } catch (Exception e) { + return this.handleError(e.getMessage(), e); } + } - private Map jsonToMap(String json) { - if (json == null) { - return new HashMap<>(); - } - - try { - return OBJECT_MAPPER.readValue(json, new TypeReference>() { - }); - } catch (IOException e) { - handleError("Could not parse json for JWTHelper", e); - return new HashMap<>(); - } + private Map jsonToMap(String json) { + if (json == null) { + return new HashMap<>(); } - private String createJWT(SignatureAlgorithm signatureAlgorithm, final String apiKey, - final Map claims, - final String payload, - final Map header) throws NoSuchAlgorithmException, InvalidKeySpecException { - - if (signatureAlgorithm == null) { - signatureAlgorithm = SignatureAlgorithm.NONE; - } - - final JwtBuilder jwtBuilder = Jwts.builder(); - - if (signatureAlgorithm != SignatureAlgorithm.NONE) { - - if (StringUtils.isBlank(apiKey)) { - throw new IllegalStateException("key must not be empty in case algo is defined"); - } - - byte[] apiKeyBytes = Base64.getDecoder().decode(apiKey); - - Key key; - switch (signatureAlgorithm.getFamilyName()) { - case "HMAC": - key = new SecretKeySpec(apiKeyBytes, signatureAlgorithm.getJcaName()); - break; - case "ECDSA": - final KeyFactory ecdsaKf = KeyFactory.getInstance("ECDSA", new BouncyCastleProvider()); - key = ecdsaKf.generatePrivate(new PKCS8EncodedKeySpec(apiKeyBytes)); - break; - case "RSA": - default: - final KeyFactory rsaKf = KeyFactory.getInstance(signatureAlgorithm.getFamilyName()); - key = rsaKf.generatePrivate(new PKCS8EncodedKeySpec(apiKeyBytes)); - break; - } - - jwtBuilder.signWith(key, signatureAlgorithm); - } - - if (claims != null && !claims.isEmpty()) { - jwtBuilder.setClaims(claims); - } else if (payload != null) { - jwtBuilder.setPayload(payload); - } - - if (header != null && !header.isEmpty()) { - jwtBuilder.setHeader(header); - } - - return jwtBuilder.compact(); + try { + return OBJECT_MAPPER.readValue(json, new TypeReference>() {}); + } catch (IOException e) { + handleError("Could not parse json for JWTHelper", e); + return new HashMap<>(); + } + } + + private String createJWT( + SignatureAlgorithm signatureAlgorithm, + final String apiKey, + final Map claims, + final String payload, + final Map header) + throws NoSuchAlgorithmException, InvalidKeySpecException { + + if (signatureAlgorithm == null) { + signatureAlgorithm = SignatureAlgorithm.NONE; } + + final JwtBuilder jwtBuilder = Jwts.builder(); + + if (signatureAlgorithm != SignatureAlgorithm.NONE) { + + if (apiKey == null || apiKey.trim().isEmpty()) { + throw new IllegalStateException("key must not be empty in case algo is defined"); + } + + byte[] apiKeyBytes = Base64.getDecoder().decode(apiKey); + + Key key; + switch (signatureAlgorithm.getFamilyName()) { + case "HMAC": + key = new SecretKeySpec(apiKeyBytes, signatureAlgorithm.getJcaName()); + break; + case "ECDSA": + final KeyFactory ecdsaKf = KeyFactory.getInstance("ECDSA", new BouncyCastleProvider()); + key = ecdsaKf.generatePrivate(new PKCS8EncodedKeySpec(apiKeyBytes)); + break; + case "RSA": + default: + final KeyFactory rsaKf = KeyFactory.getInstance(signatureAlgorithm.getFamilyName()); + key = rsaKf.generatePrivate(new PKCS8EncodedKeySpec(apiKeyBytes)); + break; + } + + jwtBuilder.signWith(key, signatureAlgorithm); + } + + if (claims != null && !claims.isEmpty()) { + jwtBuilder.setClaims(claims); + } else if (payload != null) { + jwtBuilder.setPayload(payload); + } + + if (header != null && !header.isEmpty()) { + jwtBuilder.setHeader(header); + } + + return jwtBuilder.compact(); + } } diff --git a/src/main/java/com/github/tomakehurst/wiremock/extension/responsetemplating/helpers/SystemValueHelper.java b/src/main/java/com/github/tomakehurst/wiremock/extension/responsetemplating/helpers/SystemValueHelper.java index cb8e86bc9f..81206100c6 100644 --- a/src/main/java/com/github/tomakehurst/wiremock/extension/responsetemplating/helpers/SystemValueHelper.java +++ b/src/main/java/com/github/tomakehurst/wiremock/extension/responsetemplating/helpers/SystemValueHelper.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2019-2021 Thomas Akehurst + * Copyright (C) 2019-2024 Thomas Akehurst * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -15,10 +15,11 @@ */ package com.github.tomakehurst.wiremock.extension.responsetemplating.helpers; +import static com.github.tomakehurst.wiremock.common.Strings.isEmpty; + import com.github.jknack.handlebars.Options; import com.github.tomakehurst.wiremock.extension.responsetemplating.SystemKeyAuthoriser; import java.security.AccessControlException; -import org.apache.commons.lang3.StringUtils; public class SystemValueHelper extends HandlebarsHelper { @@ -32,7 +33,8 @@ public SystemValueHelper(SystemKeyAuthoriser systemKeyAuthoriser) { public String apply(Object context, Options options) { String key = options.hash("key", ""); String type = options.hash("type", "ENVIRONMENT"); - if (StringUtils.isEmpty(key)) { + String defaultValue = options.hash("default"); + if (isEmpty(key)) { return this.handleError("The key cannot be empty"); } if (!systemKeyAuthoriser.isPermitted(key)) { @@ -44,10 +46,10 @@ public String apply(Object context, Options options) { try { switch (type) { case "ENVIRONMENT": - rawValue = getSystemEnvironment(key); + rawValue = getSystemEnvironment(key, defaultValue); break; case "PROPERTY": - rawValue = getSystemProperties(key); + rawValue = getSystemProperties(key, defaultValue); break; } return rawValue; @@ -57,11 +59,11 @@ public String apply(Object context, Options options) { } } - private String getSystemEnvironment(final String key) { - return System.getenv(key); + private String getSystemEnvironment(final String key, final String defaultValue) { + return System.getenv().getOrDefault(key, defaultValue); } - private String getSystemProperties(final String key) { - return System.getProperty(key); + private String getSystemProperties(final String key, final String defaultValue) { + return System.getProperty(key, defaultValue); } } diff --git a/src/main/java/com/github/tomakehurst/wiremock/http/AbstractRequestHandler.java b/src/main/java/com/github/tomakehurst/wiremock/http/AbstractRequestHandler.java index 9702f808d5..ab6e506be6 100644 --- a/src/main/java/com/github/tomakehurst/wiremock/http/AbstractRequestHandler.java +++ b/src/main/java/com/github/tomakehurst/wiremock/http/AbstractRequestHandler.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2011-2023 Thomas Akehurst + * Copyright (C) 2011-2024 Thomas Akehurst * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -19,6 +19,7 @@ import static com.github.tomakehurst.wiremock.stubbing.ServeEvent.ORIGINAL_SERVE_EVENT_KEY; import com.github.tomakehurst.wiremock.common.DataTruncationSettings; +import com.github.tomakehurst.wiremock.common.RequestCache; import com.github.tomakehurst.wiremock.extension.requestfilter.*; import com.github.tomakehurst.wiremock.stubbing.ServeEvent; import java.util.ArrayList; @@ -102,6 +103,8 @@ public void handle(Request request, HttpResponder httpResponder, ServeEvent orig serveEvent.afterSend(); afterResponseSent(serveEvent, response); + + RequestCache.onRequestEnd(); } protected String formatRequest(Request request) { diff --git a/src/main/java/com/github/tomakehurst/wiremock/http/ContentTypeHeader.java b/src/main/java/com/github/tomakehurst/wiremock/http/ContentTypeHeader.java index 9841dd48d5..4603b76d29 100644 --- a/src/main/java/com/github/tomakehurst/wiremock/http/ContentTypeHeader.java +++ b/src/main/java/com/github/tomakehurst/wiremock/http/ContentTypeHeader.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2011-2023 Thomas Akehurst + * Copyright (C) 2011-2024 Thomas Akehurst * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -15,7 +15,8 @@ */ package com.github.tomakehurst.wiremock.http; -import com.github.tomakehurst.wiremock.common.Strings; +import static java.nio.charset.StandardCharsets.UTF_8; + import java.nio.charset.Charset; import java.util.Optional; @@ -61,6 +62,6 @@ public Charset charset() { return Charset.forName(encodingPart().get()); } - return Strings.DEFAULT_CHARSET; + return UTF_8; } } diff --git a/src/main/java/com/github/tomakehurst/wiremock/http/HttpClientFactory.java b/src/main/java/com/github/tomakehurst/wiremock/http/HttpClientFactory.java index 71dc92b774..a19ec15778 100644 --- a/src/main/java/com/github/tomakehurst/wiremock/http/HttpClientFactory.java +++ b/src/main/java/com/github/tomakehurst/wiremock/http/HttpClientFactory.java @@ -18,10 +18,10 @@ import static com.github.tomakehurst.wiremock.common.Exceptions.throwUnchecked; import static com.github.tomakehurst.wiremock.common.LocalNotifier.notifier; import static com.github.tomakehurst.wiremock.common.ProxySettings.NO_PROXY; +import static com.github.tomakehurst.wiremock.common.Strings.isNotEmpty; import static com.github.tomakehurst.wiremock.common.ssl.KeyStoreSettings.NO_STORE; import static com.github.tomakehurst.wiremock.http.RequestMethod.*; import static java.nio.charset.StandardCharsets.UTF_8; -import static org.apache.commons.lang3.StringUtils.isEmpty; import com.github.tomakehurst.wiremock.common.NetworkAddressRules; import com.github.tomakehurst.wiremock.common.ProxySettings; @@ -107,7 +107,7 @@ public static CloseableHttpClient createClient( if (proxySettings != NO_PROXY) { HttpHost proxyHost = new HttpHost(proxySettings.host(), proxySettings.port()); builder.setProxy(proxyHost); - if (!isEmpty(proxySettings.getUsername()) && !isEmpty(proxySettings.getPassword())) { + if (isNotEmpty(proxySettings.getUsername()) && isNotEmpty(proxySettings.getPassword())) { builder.setProxyAuthenticationStrategy(new DefaultAuthenticationStrategy()); // TODO Verify BasicCredentialsProvider credentialsProvider = new BasicCredentialsProvider(); credentialsProvider.setCredentials( @@ -144,10 +144,6 @@ private static LayeredConnectionSocketFactory buildSslConnectionSocketFactory( ); } - /** - * Copied from {@link HttpClientBuilder#split(String)} which is not the same as {@link - * org.apache.commons.lang3.StringUtils#split(String)} - */ private static String[] split(final String s) { if (TextUtils.isBlank(s)) { return null; diff --git a/src/main/java/com/github/tomakehurst/wiremock/http/HttpServerFactory.java b/src/main/java/com/github/tomakehurst/wiremock/http/HttpServerFactory.java index aa4f429190..e2545304b0 100644 --- a/src/main/java/com/github/tomakehurst/wiremock/http/HttpServerFactory.java +++ b/src/main/java/com/github/tomakehurst/wiremock/http/HttpServerFactory.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2014-2023 Thomas Akehurst + * Copyright (C) 2014-2024 Thomas Akehurst * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -18,6 +18,7 @@ import com.github.tomakehurst.wiremock.core.Options; import com.github.tomakehurst.wiremock.extension.Extension; +@FunctionalInterface public interface HttpServerFactory extends Extension { @Override diff --git a/src/main/java/com/github/tomakehurst/wiremock/http/LoggedResponse.java b/src/main/java/com/github/tomakehurst/wiremock/http/LoggedResponse.java index ad05fd7227..4c5104e986 100644 --- a/src/main/java/com/github/tomakehurst/wiremock/http/LoggedResponse.java +++ b/src/main/java/com/github/tomakehurst/wiremock/http/LoggedResponse.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2016-2023 Thomas Akehurst + * Copyright (C) 2016-2024 Thomas Akehurst * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,6 +16,7 @@ package com.github.tomakehurst.wiremock.http; import static com.github.tomakehurst.wiremock.common.ContentTypes.OCTET_STREAM; +import static java.nio.charset.StandardCharsets.UTF_8; import com.fasterxml.jackson.annotation.JsonIgnore; import com.fasterxml.jackson.annotation.JsonProperty; @@ -89,7 +90,7 @@ public String getMimeType() { @JsonIgnore public Charset getCharset() { - return headers == null ? Strings.DEFAULT_CHARSET : headers.getContentTypeHeader().charset(); + return headers == null ? UTF_8 : headers.getContentTypeHeader().charset(); } @JsonIgnore diff --git a/src/main/java/com/github/tomakehurst/wiremock/http/ProxyResponseRenderer.java b/src/main/java/com/github/tomakehurst/wiremock/http/ProxyResponseRenderer.java index 2110788337..7c4f0c051b 100644 --- a/src/main/java/com/github/tomakehurst/wiremock/http/ProxyResponseRenderer.java +++ b/src/main/java/com/github/tomakehurst/wiremock/http/ProxyResponseRenderer.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2011-2023 Thomas Akehurst + * Copyright (C) 2011-2024 Thomas Akehurst * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -25,8 +25,12 @@ import com.github.tomakehurst.wiremock.stubbing.ServeEvent; import java.io.IOException; import java.net.URI; +import java.util.Arrays; +import java.util.Collections; import java.util.LinkedList; import java.util.List; +import java.util.Set; +import java.util.stream.Collectors; import javax.net.ssl.SSLException; public class ProxyResponseRenderer implements ResponseRenderer { @@ -37,7 +41,9 @@ public class ProxyResponseRenderer implements ResponseRenderer { private final String hostHeaderValue; private final SettingsStore settingsStore; private final boolean stubCorsEnabled; + private final Set supportedEncodings; + @SuppressWarnings("unused") public ProxyResponseRenderer( boolean preserveHostHeader, String hostHeaderValue, @@ -46,10 +52,30 @@ public ProxyResponseRenderer( HttpClient reverseProxyClient, HttpClient forwardProxyClient) { + this( + preserveHostHeader, + hostHeaderValue, + settingsStore, + stubCorsEnabled, + null, + reverseProxyClient, + forwardProxyClient); + } + + public ProxyResponseRenderer( + boolean preserveHostHeader, + String hostHeaderValue, + SettingsStore settingsStore, + boolean stubCorsEnabled, + Set supportedEncodings, + HttpClient reverseProxyClient, + HttpClient forwardProxyClient) { + this.settingsStore = settingsStore; this.preserveHostHeader = preserveHostHeader; this.hostHeaderValue = hostHeaderValue; this.stubCorsEnabled = stubCorsEnabled; + this.supportedEncodings = supportedEncodings; this.forwardProxyClient = forwardProxyClient; this.reverseProxyClient = reverseProxyClient; @@ -143,16 +169,25 @@ private HttpHeaders headersFrom(Response response, ResponseDefinition responseDe private void addRequestHeaders( ImmutableRequest.Builder requestBuilder, ResponseDefinition response) { Request originalRequest = response.getOriginalRequest(); + List removeProxyRequestHeaders = + response.getRemoveProxyRequestHeaders() == null + ? Collections.emptyList() + : response.getRemoveProxyRequestHeaders(); for (String key : originalRequest.getAllHeaderKeys()) { - if (!HttpClient.HOST_HEADER.equalsIgnoreCase(key) || preserveHostHeader) { - List values = originalRequest.header(key).values(); - requestBuilder.withHeader(key, values); - } else { - if (hostHeaderValue != null) { - requestBuilder.withHeader(key, hostHeaderValue); - } else if (response.getProxyBaseUrl() != null) { - requestBuilder.withHeader(key, URI.create(response.getProxyBaseUrl()).getAuthority()); - } + String lowerCaseKey = key.toLowerCase(); + if (removeProxyRequestHeaders.contains(lowerCaseKey)) { + continue; + } + switch (lowerCaseKey) { + case HttpClient.HOST_HEADER: + addHostHeader(requestBuilder, response, key, originalRequest); + break; + case HttpClient.ACCEPT_ENCODING_HEADER: + addAcceptEncodingHeader(requestBuilder, key, originalRequest); + break; + default: + copyHeader(requestBuilder, key, originalRequest); + break; } } @@ -164,6 +199,43 @@ private void addRequestHeaders( } } + private void addHostHeader( + ImmutableRequest.Builder requestBuilder, + ResponseDefinition response, + String key, + Request originalRequest) { + if (preserveHostHeader) { + copyHeader(requestBuilder, key, originalRequest); + } else if (hostHeaderValue != null) { + requestBuilder.withHeader(key, hostHeaderValue); + } else if (response.getProxyBaseUrl() != null) { + requestBuilder.withHeader(key, URI.create(response.getProxyBaseUrl()).getAuthority()); + } + } + + private void addAcceptEncodingHeader( + ImmutableRequest.Builder requestBuilder, String key, Request originalRequest) { + if (supportedEncodings == null) { + copyHeader(requestBuilder, key, originalRequest); + } else { + List prunedAcceptEncodings = + originalRequest.header(key).values().stream() + .flatMap(s -> Arrays.stream(s.split(","))) + .map(String::trim) + .filter(supportedEncodings::contains) + .collect(Collectors.toList()); + if (!prunedAcceptEncodings.isEmpty()) { + requestBuilder.withHeader(key, String.join(",", prunedAcceptEncodings)); + } + } + } + + private static void copyHeader( + ImmutableRequest.Builder requestBuilder, String key, Request originalRequest) { + List values = originalRequest.header(key).values(); + requestBuilder.withHeader(key, values); + } + public boolean responseHeaderShouldBeTransferred(String key) { final String lowerCaseKey = key.toLowerCase(); return !HttpClient.FORBIDDEN_RESPONSE_HEADERS.contains(lowerCaseKey) diff --git a/src/main/java/com/github/tomakehurst/wiremock/http/ResponseDefinition.java b/src/main/java/com/github/tomakehurst/wiremock/http/ResponseDefinition.java index f25aabc63d..f6d78a824e 100644 --- a/src/main/java/com/github/tomakehurst/wiremock/http/ResponseDefinition.java +++ b/src/main/java/com/github/tomakehurst/wiremock/http/ResponseDefinition.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2011-2023 Thomas Akehurst + * Copyright (C) 2011-2024 Thomas Akehurst * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -18,6 +18,7 @@ import static com.fasterxml.jackson.annotation.JsonInclude.Include.NON_EMPTY; import static com.github.tomakehurst.wiremock.common.ContentTypes.CONTENT_TYPE; import static com.github.tomakehurst.wiremock.common.ContentTypes.LOCATION; +import static com.github.tomakehurst.wiremock.common.Strings.removeStart; import static java.net.HttpURLConnection.*; import com.fasterxml.jackson.annotation.JsonCreator; @@ -34,7 +35,6 @@ import java.util.List; import java.util.Objects; import java.util.Optional; -import org.apache.commons.lang3.StringUtils; public class ResponseDefinition { @@ -44,6 +44,7 @@ public class ResponseDefinition { private final String bodyFileName; private final HttpHeaders headers; private final HttpHeaders additionalProxyRequestHeaders; + private final List removeProxyRequestHeaders; private final Integer fixedDelayMilliseconds; private final DelayDistribution delayDistribution; private final ChunkedDribbleDelay chunkedDribbleDelay; @@ -67,6 +68,7 @@ public ResponseDefinition( @JsonProperty("bodyFileName") String bodyFileName, @JsonProperty("headers") HttpHeaders headers, @JsonProperty("additionalProxyRequestHeaders") HttpHeaders additionalProxyRequestHeaders, + @JsonProperty("removeProxyRequestHeaders") List removeProxyRequestHeaders, @JsonProperty("fixedDelayMilliseconds") Integer fixedDelayMilliseconds, @JsonProperty("delayDistribution") DelayDistribution delayDistribution, @JsonProperty("chunkedDribbleDelay") ChunkedDribbleDelay chunkedDribbleDelay, @@ -83,6 +85,7 @@ public ResponseDefinition( bodyFileName, headers, additionalProxyRequestHeaders, + removeProxyRequestHeaders, fixedDelayMilliseconds, delayDistribution, chunkedDribbleDelay, @@ -103,6 +106,7 @@ public ResponseDefinition( String bodyFileName, HttpHeaders headers, HttpHeaders additionalProxyRequestHeaders, + List removeProxyRequestHeaders, Integer fixedDelayMilliseconds, DelayDistribution delayDistribution, ChunkedDribbleDelay chunkedDribbleDelay, @@ -119,6 +123,7 @@ public ResponseDefinition( bodyFileName, headers, additionalProxyRequestHeaders, + removeProxyRequestHeaders, fixedDelayMilliseconds, delayDistribution, chunkedDribbleDelay, @@ -137,6 +142,7 @@ public ResponseDefinition( String bodyFileName, HttpHeaders headers, HttpHeaders additionalProxyRequestHeaders, + List removeProxyRequestHeaders, Integer fixedDelayMilliseconds, DelayDistribution delayDistribution, ChunkedDribbleDelay chunkedDribbleDelay, @@ -154,6 +160,7 @@ public ResponseDefinition( this.headers = headers; this.additionalProxyRequestHeaders = additionalProxyRequestHeaders; + this.removeProxyRequestHeaders = removeProxyRequestHeaders; this.fixedDelayMilliseconds = fixedDelayMilliseconds; this.delayDistribution = delayDistribution; this.chunkedDribbleDelay = chunkedDribbleDelay; @@ -179,6 +186,7 @@ public ResponseDefinition(final int statusCode, final String bodyContent) { null, null, null, + null, Collections.emptyList(), Parameters.empty(), true); @@ -198,6 +206,7 @@ public ResponseDefinition(final int statusCode, final byte[] bodyContent) { null, null, null, + null, Collections.emptyList(), Parameters.empty(), true); @@ -217,6 +226,7 @@ public ResponseDefinition() { null, null, null, + null, Collections.emptyList(), Parameters.empty(), true); @@ -306,6 +316,7 @@ public ResponseDefinition copy() { this.bodyFileName, this.headers, this.additionalProxyRequestHeaders, + this.removeProxyRequestHeaders, this.fixedDelayMilliseconds, this.delayDistribution, this.chunkedDribbleDelay, @@ -326,6 +337,10 @@ public HttpHeaders getAdditionalProxyRequestHeaders() { return additionalProxyRequestHeaders; } + public List getRemoveProxyRequestHeaders() { + return removeProxyRequestHeaders; + } + public int getStatus() { return status; } @@ -398,8 +413,8 @@ public String getProxyUrl() { } String originalRequestUrl = - Optional.ofNullable(originalRequest).map(Request::getUrl).orElse(StringUtils.EMPTY); - return proxyBaseUrl + StringUtils.removeStart(originalRequestUrl, proxyUrlPrefixToRemove); + Optional.ofNullable(originalRequest).map(Request::getUrl).orElse(""); + return proxyBaseUrl + removeStart(originalRequestUrl, proxyUrlPrefixToRemove); } public String getProxyBaseUrl() { @@ -473,6 +488,7 @@ public boolean equals(Object o) { && Objects.equals(bodyFileName, that.bodyFileName) && Objects.equals(headers, that.headers) && Objects.equals(additionalProxyRequestHeaders, that.additionalProxyRequestHeaders) + && Objects.equals(removeProxyRequestHeaders, that.removeProxyRequestHeaders) && Objects.equals(fixedDelayMilliseconds, that.fixedDelayMilliseconds) && Objects.equals(delayDistribution, that.delayDistribution) && Objects.equals(chunkedDribbleDelay, that.chunkedDribbleDelay) @@ -494,6 +510,7 @@ public int hashCode() { bodyFileName, headers, additionalProxyRequestHeaders, + removeProxyRequestHeaders, fixedDelayMilliseconds, delayDistribution, chunkedDribbleDelay, diff --git a/src/main/java/com/github/tomakehurst/wiremock/http/client/HttpClient.java b/src/main/java/com/github/tomakehurst/wiremock/http/client/HttpClient.java index 403e1230cb..b74feecb19 100644 --- a/src/main/java/com/github/tomakehurst/wiremock/http/client/HttpClient.java +++ b/src/main/java/com/github/tomakehurst/wiremock/http/client/HttpClient.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2023 Thomas Akehurst + * Copyright (C) 2023-2024 Thomas Akehurst * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -30,6 +30,7 @@ public interface HttpClient { List FORBIDDEN_REQUEST_HEADERS = List.of(TRANSFER_ENCODING, CONTENT_LENGTH, "connection", USER_AGENT); String HOST_HEADER = "host"; + String ACCEPT_ENCODING_HEADER = "accept-encoding"; Response execute(Request request) throws IOException; } diff --git a/src/main/java/com/github/tomakehurst/wiremock/http/trafficlistener/WiremockNetworkTrafficListeners.java b/src/main/java/com/github/tomakehurst/wiremock/http/trafficlistener/WiremockNetworkTrafficListeners.java index c63e623202..992191623b 100644 --- a/src/main/java/com/github/tomakehurst/wiremock/http/trafficlistener/WiremockNetworkTrafficListeners.java +++ b/src/main/java/com/github/tomakehurst/wiremock/http/trafficlistener/WiremockNetworkTrafficListeners.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2023 Thomas Akehurst + * Copyright (C) 2023-2024 Thomas Akehurst * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -15,14 +15,14 @@ */ package com.github.tomakehurst.wiremock.http.trafficlistener; +import static java.nio.charset.StandardCharsets.UTF_8; + import com.github.tomakehurst.wiremock.common.ConsoleNotifier; import com.github.tomakehurst.wiremock.common.Notifier; import java.nio.charset.Charset; -import java.nio.charset.StandardCharsets; public final class WiremockNetworkTrafficListeners { private static final ConsoleNotifier CONSOLE_NOTIFIER = new ConsoleNotifier(true); - private static final Charset DEFAULT_CHARSET = StandardCharsets.UTF_8; private WiremockNetworkTrafficListeners() {} @@ -31,7 +31,7 @@ public static WiremockNetworkTrafficListener createNotifying(Notifier notifier, } public static WiremockNetworkTrafficListener createConsoleNotifying() { - return new NotifyingWiremockNetworkTrafficListener(CONSOLE_NOTIFIER, DEFAULT_CHARSET); + return new NotifyingWiremockNetworkTrafficListener(CONSOLE_NOTIFIER, UTF_8); } public static WiremockNetworkTrafficListener createConsoleNotifying(Charset charset) { diff --git a/src/main/java/com/github/tomakehurst/wiremock/jetty/JettyFaultInjector.java b/src/main/java/com/github/tomakehurst/wiremock/jetty/JettyFaultInjector.java index 133a53eb63..087a12748d 100644 --- a/src/main/java/com/github/tomakehurst/wiremock/jetty/JettyFaultInjector.java +++ b/src/main/java/com/github/tomakehurst/wiremock/jetty/JettyFaultInjector.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2014-2023 Thomas Akehurst + * Copyright (C) 2014-2024 Thomas Akehurst * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,16 +16,12 @@ package com.github.tomakehurst.wiremock.jetty; import static com.github.tomakehurst.wiremock.common.Exceptions.throwUnchecked; -import static com.github.tomakehurst.wiremock.jetty.JettyUtils.unwrapResponse; import static java.nio.charset.StandardCharsets.UTF_8; import com.github.tomakehurst.wiremock.core.FaultInjector; import jakarta.servlet.http.HttpServletResponse; import java.io.IOException; import java.net.Socket; -import java.nio.channels.SocketChannel; -import org.eclipse.jetty.io.SelectableChannelEndPoint; -import org.eclipse.jetty.server.HttpChannel; import org.eclipse.jetty.server.Response; import org.eclipse.jetty.util.BufferUtil; @@ -33,12 +29,13 @@ public class JettyFaultInjector implements FaultInjector { private static final byte[] GARBAGE = "lskdu018973t09sylgasjkfg1][]'./.sdlv".getBytes(UTF_8); - private final Response response; + private final HttpServletResponse response; private final Socket socket; - public JettyFaultInjector(HttpServletResponse response) { - this.response = unwrapResponse(response); - this.socket = socket(); + public JettyFaultInjector(HttpServletResponse response, JettyHttpUtils utils) { + this.response = response; + final Response jettyResponse = utils.unwrapResponse(response); + this.socket = utils.socket(jettyResponse); } @Override @@ -81,10 +78,4 @@ public void randomDataAndCloseConnection() { throwUnchecked(e); } } - - private Socket socket() { - HttpChannel httpChannel = response.getHttpOutput().getHttpChannel(); - SelectableChannelEndPoint ep = (SelectableChannelEndPoint) httpChannel.getEndPoint(); - return ((SocketChannel) ep.getChannel()).socket(); - } } diff --git a/src/main/java/com/github/tomakehurst/wiremock/jetty/JettyFaultInjectorFactory.java b/src/main/java/com/github/tomakehurst/wiremock/jetty/JettyFaultInjectorFactory.java index 0599b216df..4d107676df 100644 --- a/src/main/java/com/github/tomakehurst/wiremock/jetty/JettyFaultInjectorFactory.java +++ b/src/main/java/com/github/tomakehurst/wiremock/jetty/JettyFaultInjectorFactory.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2015-2022 Thomas Akehurst + * Copyright (C) 2015-2024 Thomas Akehurst * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -21,14 +21,19 @@ import jakarta.servlet.http.HttpServletResponse; public class JettyFaultInjectorFactory implements FaultInjectorFactory { + private final JettyHttpUtils utils; + + public JettyFaultInjectorFactory(JettyHttpUtils utils) { + this.utils = utils; + } @Override public FaultInjector buildFaultInjector( HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse) { if (httpServletRequest.getScheme().equals("https")) { - return new JettyHttpsFaultInjector(httpServletResponse); + return new JettyHttpsFaultInjector(httpServletResponse, utils); } - return new JettyFaultInjector(httpServletResponse); + return new JettyFaultInjector(httpServletResponse, utils); } } diff --git a/src/main/java/com/github/tomakehurst/wiremock/jetty/JettyHttpServer.java b/src/main/java/com/github/tomakehurst/wiremock/jetty/JettyHttpServer.java index 14354bd2d3..144f25da0e 100644 --- a/src/main/java/com/github/tomakehurst/wiremock/jetty/JettyHttpServer.java +++ b/src/main/java/com/github/tomakehurst/wiremock/jetty/JettyHttpServer.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2014-2023 Thomas Akehurst + * Copyright (C) 2014-2024 Thomas Akehurst * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,58 +16,38 @@ package com.github.tomakehurst.wiremock.jetty; import static com.github.tomakehurst.wiremock.common.Exceptions.throwUnchecked; -import static com.github.tomakehurst.wiremock.common.ResourceUtil.getResource; -import static com.github.tomakehurst.wiremock.core.WireMockApp.ADMIN_CONTEXT_ROOT; -import static java.util.concurrent.Executors.newScheduledThreadPool; import com.github.tomakehurst.wiremock.common.*; import com.github.tomakehurst.wiremock.core.Options; import com.github.tomakehurst.wiremock.core.WireMockApp; import com.github.tomakehurst.wiremock.http.AdminRequestHandler; import com.github.tomakehurst.wiremock.http.HttpServer; -import com.github.tomakehurst.wiremock.http.RequestHandler; import com.github.tomakehurst.wiremock.http.StubRequestHandler; import com.github.tomakehurst.wiremock.http.trafficlistener.WiremockNetworkTrafficListener; -import com.github.tomakehurst.wiremock.jetty.websockets.WebSocketEndpoint; import com.github.tomakehurst.wiremock.servlet.*; -import jakarta.servlet.DispatcherType; -import jakarta.servlet.http.HttpServletRequest; -import jakarta.servlet.http.HttpServletResponse; import java.io.IOException; import java.net.Socket; import java.nio.ByteBuffer; -import java.util.EnumSet; -import java.util.Map; import java.util.Optional; import java.util.concurrent.ScheduledExecutorService; import java.util.concurrent.TimeoutException; -import org.apache.commons.lang3.ArrayUtils; -import org.apache.commons.lang3.mutable.MutableBoolean; -import org.eclipse.jetty.http.MimeTypes; +import java.util.concurrent.atomic.AtomicBoolean; import org.eclipse.jetty.io.EndPoint; import org.eclipse.jetty.io.NetworkTrafficListener; -import org.eclipse.jetty.rewrite.handler.RewriteHandler; -import org.eclipse.jetty.rewrite.handler.RewriteRegexRule; import org.eclipse.jetty.server.Handler; -import org.eclipse.jetty.server.Request; import org.eclipse.jetty.server.Server; import org.eclipse.jetty.server.ServerConnector; -import org.eclipse.jetty.server.handler.AbstractHandler; -import org.eclipse.jetty.server.handler.HandlerCollection; -import org.eclipse.jetty.server.handler.gzip.GzipHandler; -import org.eclipse.jetty.servlet.DefaultServlet; -import org.eclipse.jetty.servlet.FilterHolder; -import org.eclipse.jetty.servlet.ServletContextHandler; -import org.eclipse.jetty.servlet.ServletHolder; -import org.eclipse.jetty.servlets.CrossOriginFilter; -import org.eclipse.jetty.websocket.jakarta.server.config.JakartaWebSocketServletContainerInitializer; public abstract class JettyHttpServer implements HttpServer { - private static final String FILES_URL_MATCH = String.format("/%s/*", WireMockApp.FILES_ROOT); - private static final String[] GZIPPABLE_METHODS = new String[] {"POST", "PUT", "PATCH", "DELETE"}; - private static final MutableBoolean STRICT_HTTP_HEADERS_APPLIED = new MutableBoolean(false); + private static final AtomicBoolean STRICT_HTTP_HEADERS_APPLIED = new AtomicBoolean(false); private static final int MAX_RETRIES = 3; + protected static final String FILES_URL_MATCH = String.format("/%s/*", WireMockApp.FILES_ROOT); + protected static final String[] GZIPPABLE_METHODS = + new String[] {"POST", "PUT", "PATCH", "DELETE"}; + + protected final Options options; + protected final Server jettyServer; protected final ServerConnector httpConnector; protected final ServerConnector httpsConnector; @@ -78,9 +58,12 @@ public JettyHttpServer( Options options, AdminRequestHandler adminRequestHandler, StubRequestHandler stubRequestHandler) { - if (!options.getDisableStrictHttpHeaders() && STRICT_HTTP_HEADERS_APPLIED.isFalse()) { + this.options = options; + + if (!options.getDisableStrictHttpHeaders() + && Boolean.FALSE.equals(STRICT_HTTP_HEADERS_APPLIED.get())) { System.setProperty("org.eclipse.jetty.http.HttpGenerator.STRICT", String.valueOf(true)); - STRICT_HTTP_HEADERS_APPLIED.setTrue(); + STRICT_HTTP_HEADERS_APPLIED.set(true); } jettyServer = createServer(options); @@ -114,8 +97,7 @@ public JettyHttpServer( applyAdditionalServerConfiguration(jettyServer, options); - final HandlerCollection handlers = - createHandler(options, adminRequestHandler, stubRequestHandler); + final Handler handlers = createHandler(options, adminRequestHandler, stubRequestHandler); jettyServer.setHandler(handlers); finalizeSetup(options); @@ -123,64 +105,10 @@ public JettyHttpServer( protected void applyAdditionalServerConfiguration(Server jettyServer, Options options) {} - protected HandlerCollection createHandler( + protected abstract Handler createHandler( Options options, AdminRequestHandler adminRequestHandler, - StubRequestHandler stubRequestHandler) { - Notifier notifier = options.notifier(); - ServletContextHandler adminContext = addAdminContext(adminRequestHandler, notifier); - ServletContextHandler mockServiceContext = - addMockServiceContext( - stubRequestHandler, - options.filesRoot(), - options.getAsynchronousResponseSettings(), - options.getChunkedEncodingPolicy(), - options.getStubCorsEnabled(), - options.browserProxySettings().enabled(), - notifier); - - HandlerCollection handlers = new HandlerCollection(); - AbstractHandler asyncTimeoutSettingHandler = - new AbstractHandler() { - @Override - public void handle( - final String target, - final Request baseRequest, - final HttpServletRequest request, - final HttpServletResponse response) { - baseRequest.getHttpChannel().getState().setTimeout(options.timeout()); - } - }; - - handlers.setHandlers( - ArrayUtils.addAll(extensionHandlers(), adminContext, asyncTimeoutSettingHandler)); - - // We prepend the rewrite handle for the wiremock gui web app to make sure that we route the - // requests properly - final RewriteHandler rewriteHandler = webAppRewriteContext(adminContext); - handlers.prependHandler(rewriteHandler); - - if (options.getGzipDisabled()) { - handlers.addHandler(mockServiceContext); - } else { - addGZipHandler(mockServiceContext, handlers); - } - - return handlers; - } - - private void addGZipHandler( - ServletContextHandler mockServiceContext, HandlerCollection handlers) { - try { - GzipHandler gzipHandler = new GzipHandler(); - gzipHandler.addIncludedMethods(GZIPPABLE_METHODS); - gzipHandler.setHandler(mockServiceContext); - gzipHandler.setVary(null); - handlers.addHandler(gzipHandler); - } catch (Exception e) { - throwUnchecked(e); - } - } + StubRequestHandler stubRequestHandler); protected void finalizeSetup(Options options) { if (options.jettySettings().getStopTimeout().isEmpty()) { @@ -198,7 +126,7 @@ protected Server createServer(Options options) { } /** Extend only this method if you want to add additional handlers to Jetty. */ - protected Handler[] extensionHandlers() { + public Handler[] extensionHandlers() { return new Handler[] {}; } @@ -283,189 +211,6 @@ protected abstract ServerConnector createHttpsConnector( JettySettings jettySettings, NetworkTrafficListener listener); - private ServletContextHandler addMockServiceContext( - StubRequestHandler stubRequestHandler, - FileSource fileSource, - AsynchronousResponseSettings asynchronousResponseSettings, - Options.ChunkedEncodingPolicy chunkedEncodingPolicy, - boolean stubCorsEnabled, - boolean browserProxyingEnabled, - Notifier notifier) { - ServletContextHandler mockServiceContext = new ServletContextHandler(jettyServer, "/"); - - decorateMockServiceContextBeforeConfig(mockServiceContext); - - mockServiceContext.setInitParameter("org.eclipse.jetty.servlet.Default.maxCacheSize", "0"); - mockServiceContext.setInitParameter( - "org.eclipse.jetty.servlet.Default.resourceBase", fileSource.getPath()); - mockServiceContext.setInitParameter("org.eclipse.jetty.servlet.Default.dirAllowed", "false"); - - mockServiceContext.addServlet(DefaultServlet.class, FILES_URL_MATCH); - - mockServiceContext.setAttribute( - JettyFaultInjectorFactory.class.getName(), new JettyFaultInjectorFactory()); - mockServiceContext.setAttribute(StubRequestHandler.class.getName(), stubRequestHandler); - mockServiceContext.setAttribute(Notifier.KEY, notifier); - mockServiceContext.setAttribute( - Options.ChunkedEncodingPolicy.class.getName(), chunkedEncodingPolicy); - mockServiceContext.setAttribute("browserProxyingEnabled", browserProxyingEnabled); - ServletHolder servletHolder = - mockServiceContext.addServlet(WireMockHandlerDispatchingServlet.class, "/"); - servletHolder.setInitOrder(1); - servletHolder.setInitParameter( - RequestHandler.HANDLER_CLASS_KEY, StubRequestHandler.class.getName()); - servletHolder.setInitParameter( - FaultInjectorFactory.INJECTOR_CLASS_KEY, JettyFaultInjectorFactory.class.getName()); - servletHolder.setInitParameter( - WireMockHandlerDispatchingServlet.SHOULD_FORWARD_TO_FILES_CONTEXT, "true"); - - if (asynchronousResponseSettings.isEnabled()) { - scheduledExecutorService = newScheduledThreadPool(asynchronousResponseSettings.getThreads()); - mockServiceContext.setAttribute( - WireMockHandlerDispatchingServlet.ASYNCHRONOUS_RESPONSE_EXECUTOR, - scheduledExecutorService); - } - - mockServiceContext.setAttribute( - MultipartRequestConfigurer.KEY, buildMultipartRequestConfigurer()); - - MimeTypes mimeTypes = new MimeTypes(); - mimeTypes.addMimeMapping("json", "application/json"); - mimeTypes.addMimeMapping("html", "text/html"); - mimeTypes.addMimeMapping("xml", "application/xml"); - mimeTypes.addMimeMapping("txt", "text/plain"); - mockServiceContext.setMimeTypes(mimeTypes); - mockServiceContext.setWelcomeFiles( - new String[] {"index.json", "index.html", "index.xml", "index.txt"}); - - NotFoundHandler errorHandler = new NotFoundHandler(mockServiceContext); - mockServiceContext.setErrorHandler(errorHandler); - - mockServiceContext.addFilter( - ContentTypeSettingFilter.class, FILES_URL_MATCH, EnumSet.of(DispatcherType.FORWARD)); - mockServiceContext.addFilter( - TrailingSlashFilter.class, FILES_URL_MATCH, EnumSet.allOf(DispatcherType.class)); - - if (stubCorsEnabled) { - addCorsFilter(mockServiceContext); - } - - decorateMockServiceContextAfterConfig(mockServiceContext); - - return mockServiceContext; - } - - protected void decorateMockServiceContextBeforeConfig(ServletContextHandler mockServiceContext) {} - - protected void decorateMockServiceContextAfterConfig(ServletContextHandler mockServiceContext) {} - - private ServletContextHandler addAdminContext( - AdminRequestHandler adminRequestHandler, Notifier notifier) { - ServletContextHandler adminContext = new ServletContextHandler(jettyServer, ADMIN_CONTEXT_ROOT); - - decorateAdminServiceContextBeforeConfig(adminContext); - - adminContext.setInitParameter("org.eclipse.jetty.servlet.Default.maxCacheSize", "0"); - - String javaVendor = System.getProperty("java.vendor"); - if (javaVendor != null && javaVendor.toLowerCase().contains("android")) { - // Special case for Android, fixes IllegalArgumentException("resource assets not found."): - // The Android ClassLoader apparently does not resolve directories. - // Furthermore, lib assets will be merged into a single asset directory when a jar file is - // assimilated into an apk. - // As resources can be addressed like "assets/swagger-ui/index.html", a static path element - // will suffice. - adminContext.setInitParameter("org.eclipse.jetty.servlet.Default.resourceBase", "assets"); - } else { - adminContext.setInitParameter( - "org.eclipse.jetty.servlet.Default.resourceBase", - getResource(JettyHttpServer.class, "assets").toString()); - } - - getResource(JettyHttpServer.class, "assets/swagger-ui/index.html"); - - adminContext.setInitParameter("org.eclipse.jetty.servlet.Default.dirAllowed", "false"); - ServletHolder swaggerUiServletHolder = - adminContext.addServlet(DefaultServlet.class, "/swagger-ui/*"); - swaggerUiServletHolder.setAsyncSupported(false); - adminContext.addServlet(DefaultServlet.class, "/recorder/*"); - - ServletHolder servletHolder = - adminContext.addServlet(WireMockHandlerDispatchingServlet.class, "/"); - servletHolder.setInitParameter( - RequestHandler.HANDLER_CLASS_KEY, AdminRequestHandler.class.getName()); - adminContext.setAttribute(AdminRequestHandler.class.getName(), adminRequestHandler); - adminContext.setAttribute(Notifier.KEY, notifier); - - adminContext.setAttribute(MultipartRequestConfigurer.KEY, buildMultipartRequestConfigurer()); - - adminContext.addServlet(NotMatchedServlet.class, "/not-matched"); - - addCorsFilter(adminContext); - - // Include wiremock-gui into admin context - ServletHolder webapp = adminContext.addServlet(DefaultServlet.class, "/webapp/*"); - webapp.setAsyncSupported(false); - - // Include wiremock-gui websocket into admin context - JakartaWebSocketServletContainerInitializer.configure( - adminContext, - (servletContext, serverContainer) -> { - serverContainer.addEndpoint(WebSocketEndpoint.class); - }); - - decorateAdminServiceContextAfterConfig(adminContext); - - return adminContext; - } - - protected void decorateAdminServiceContextBeforeConfig( - ServletContextHandler adminServiceContext) {} - - protected void decorateAdminServiceContextAfterConfig( - ServletContextHandler adminServiceContext) {} - - /** - * Rewrite web app. We use Angular. We must rewrite every /__admin/webapp path to the index.html - * of our single page application. - * - * @param adminContextHandler admin context handler is used to provide the static file content - * @return the rewrite handler - */ - private RewriteHandler webAppRewriteContext(ServletContextHandler adminContextHandler) { - final RewriteHandler rewrite = new RewriteHandler(); - rewrite.setRewriteRequestURI(true); - rewrite.setRewritePathInfo(true); - - RewriteRegexRule rewriteRule = new RewriteRegexRule(); - rewriteRule.setRegex("/__admin/webapp/(mappings|matched|unmatched|state|files).*"); - rewriteRule.setReplacement("/__admin/webapp/index.html"); - rewrite.addRule(rewriteRule); - - rewrite.setHandler(adminContextHandler); - - return rewrite; - } - - private void addCorsFilter(ServletContextHandler context) { - context.addFilter(buildCorsFilter(), "/*", EnumSet.of(DispatcherType.REQUEST)); - } - - private FilterHolder buildCorsFilter() { - FilterHolder filterHolder = new FilterHolder(CrossOriginFilter.class); - filterHolder.setInitParameters( - Map.of( - "chainPreflight", - "false", - "allowedOrigins", - "*", - "allowedHeaders", - "*", - "allowedMethods", - "OPTIONS,GET,POST,PUT,PATCH,DELETE")); - return filterHolder; - } - // Override this for platform-specific impls protected MultipartRequestConfigurer buildMultipartRequestConfigurer() { return new DefaultMultipartRequestConfigurer(); diff --git a/src/main/java/com/github/tomakehurst/wiremock/jetty/JettyHttpServerFactory.java b/src/main/java/com/github/tomakehurst/wiremock/jetty/JettyHttpServerFactory.java index 0099cd95e0..c356621d32 100644 --- a/src/main/java/com/github/tomakehurst/wiremock/jetty/JettyHttpServerFactory.java +++ b/src/main/java/com/github/tomakehurst/wiremock/jetty/JettyHttpServerFactory.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2014-2022 Thomas Akehurst + * Copyright (C) 2014-2024 Thomas Akehurst * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/src/main/java/com/github/tomakehurst/wiremock/jetty/JettyHttpServerFactoryLoader.java b/src/main/java/com/github/tomakehurst/wiremock/jetty/JettyHttpServerFactoryLoader.java new file mode 100644 index 0000000000..abdbca4d0c --- /dev/null +++ b/src/main/java/com/github/tomakehurst/wiremock/jetty/JettyHttpServerFactoryLoader.java @@ -0,0 +1,79 @@ +/* + * Copyright (C) 2014-2024 Thomas Akehurst + * + * 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 + * + * http://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. + */ +package com.github.tomakehurst.wiremock.jetty; + +import com.github.tomakehurst.wiremock.http.HttpServerFactory; +import java.util.Optional; +import java.util.ServiceConfigurationError; +import java.util.ServiceLoader; +import org.eclipse.jetty.util.Jetty; + +/** + * Jetty's version-dependent {@link HttpServerFactory} loader that accepts the Jetty's major version + * into account while creating the factory instance. + */ +public interface JettyHttpServerFactoryLoader { + /** + * Jetty's version-dependent {@link HttpServerFactory} loader that accepts the Jetty's major + * version into account while creating the factory instance. + * + * @param jettyMajorVersion Jetty's major version + * @return non-empty {@link Optional} if the loader support this Jetty version, {@code + * Optional.empty()} otherwise. + */ + Optional getHttpServerFactory(String jettyMajorVersion); + + /** + * Create the Jetty's version-dependent {@link HttpServerFactory} instance using Java's {@link + * ServiceLoader} mechanism or throws an exception if none of the factories could be created. + * + * @param jettyVersion Jetty version at runtime + * @return {@link HttpServerFactory} instance + */ + static HttpServerFactory create(String jettyVersion) { + final String[] version = Jetty.VERSION.split("[.]"); + if (version.length == 0 || version[0].isBlank()) { + throw new IllegalArgumentException( + "Unrecognized Jetty version: " + + jettyVersion + + ". Please make sure the right Jetty dependencies are on the classpath."); + } + + final String jettyMajorVersion = version[0]; + final ServiceLoader loaders = + ServiceLoader.load(JettyHttpServerFactoryLoader.class); + + Throwable cause = null; + try { + for (final JettyHttpServerFactoryLoader loader : loaders) { + final Optional factoryOpt = + loader.getHttpServerFactory(jettyMajorVersion); + if (factoryOpt.isPresent()) { + return factoryOpt.get(); + } + } + } catch (final ServiceConfigurationError ex) { + /* only catch this kind of exception, the Jetty's HttpServerFactoryLoader could not be instantiated */ + cause = ex; + } + + throw new IllegalStateException( + "Unable to find JettyHttpServerFactoryLoader for Jetty version " + + jettyMajorVersion + + " (only Jetty 11/12 are supported at the moment). Please make sure the right Jetty dependencies are on the classpath.", + cause); + } +} diff --git a/src/main/java/com/github/tomakehurst/wiremock/jetty/JettyHttpUtils.java b/src/main/java/com/github/tomakehurst/wiremock/jetty/JettyHttpUtils.java new file mode 100644 index 0000000000..d865339e97 --- /dev/null +++ b/src/main/java/com/github/tomakehurst/wiremock/jetty/JettyHttpUtils.java @@ -0,0 +1,93 @@ +/* + * Copyright (C) 2015-2024 Thomas Akehurst + * + * 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 + * + * http://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. + */ +package com.github.tomakehurst.wiremock.jetty; + +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import java.net.Socket; +import org.eclipse.jetty.io.EndPoint; +import org.eclipse.jetty.server.Response; + +/** Helper utility interface to inject Jetty 11/12/... specific response / request handling */ +public interface JettyHttpUtils { + static final boolean IS_JETTY = isClassExist("org.eclipse.jetty.server.Request"); + + static boolean isJetty() { + return IS_JETTY; + } + + private static boolean isClassExist(String type) { + try { + ClassLoader contextCL = Thread.currentThread().getContextClassLoader(); + ClassLoader loader = contextCL == null ? JettyHttpUtils.class.getClassLoader() : contextCL; + Class.forName(type, false, loader); + return true; + } catch (Exception e) { + return false; + } + } + + /** + * Unwraps Jetty's {@link Response} out of the {@link HttpServletResponse} + * + * @param httpServletResponse {@link HttpServletResponse} instance + * @return unwrapped {@link Response} instance + */ + Response unwrapResponse(HttpServletResponse httpServletResponse); + + /** + * Extracts the raw network socket of out Jetty's {@link Response} + * + * @param response {@link Response} instance + * @return raw network socket + */ + Socket socket(Response response); + + /** + * Sets the {@link HttpServletResponse} status and reason (if supported), depending on Jetty + * version. + * + * @param status status + * @param reason reason + * @param httpServletResponse {@link HttpServletResponse} instance to set status and reason (if + * supported) + */ + void setStatusWithReason(int status, String reason, HttpServletResponse httpServletResponse); + + /** + * Extracts the raw network TLS socket of out Jetty's {@link Response} + * + * @param response {@link Response} instance + * @return raw network TLS socket + */ + Socket tlsSocket(Response response); + + /** + * Unwraps Jetty's {@link EndPoint} out of the {@link Response} + * + * @param response {@link Response} instance + * @return unwrapped {@link EndPoint} instance + */ + EndPoint unwrapEndPoint(Response response); + + /** + * Checks if the {@link HttpServletRequest} is a browser proxy request + * + * @param request {@link HttpServletRequest} instance + * @return {@code true} if is a request isbrowser proxy request, {@code false} otherwise + */ + boolean isBrowserProxyRequest(HttpServletRequest request); +} diff --git a/src/main/java/com/github/tomakehurst/wiremock/jetty/JettyHttpsFaultInjector.java b/src/main/java/com/github/tomakehurst/wiremock/jetty/JettyHttpsFaultInjector.java index eaf9a1ed84..e00b22726b 100644 --- a/src/main/java/com/github/tomakehurst/wiremock/jetty/JettyHttpsFaultInjector.java +++ b/src/main/java/com/github/tomakehurst/wiremock/jetty/JettyHttpsFaultInjector.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2014-2023 Thomas Akehurst + * Copyright (C) 2014-2024 Thomas Akehurst * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -23,6 +23,7 @@ import jakarta.servlet.http.HttpServletResponse; import java.io.IOException; import java.net.Socket; +import org.eclipse.jetty.io.EndPoint; import org.eclipse.jetty.server.Response; import org.eclipse.jetty.util.BufferUtil; import org.eclipse.jetty.util.Callback; @@ -31,12 +32,15 @@ public class JettyHttpsFaultInjector implements FaultInjector { private static final byte[] GARBAGE = "lskdu018973t09sylgasjkfg1][]'./.sdlv".getBytes(UTF_8); - private final Response response; + private final HttpServletResponse response; + private final EndPoint endpoint; private final Socket socket; - public JettyHttpsFaultInjector(HttpServletResponse response) { - this.response = JettyUtils.unwrapResponse(response); - this.socket = JettyUtils.getTlsSocket(this.response); + public JettyHttpsFaultInjector(HttpServletResponse response, JettyHttpUtils utils) { + this.response = response; + final Response jettyResponse = utils.unwrapResponse(response); + this.endpoint = utils.unwrapEndPoint(jettyResponse); + this.socket = utils.tlsSocket(jettyResponse); } @Override @@ -75,30 +79,26 @@ public void randomDataAndCloseConnection() { } private void writeGarbageThenCloseSocket() { - response - .getHttpOutput() - .getHttpChannel() - .getEndPoint() - .write( - new Callback() { - @Override - public void succeeded() { - try { - socket.close(); - } catch (IOException e) { - notifier().error("Failed to close socket after Garbage write succeeded", e); - } - } + endpoint.write( + new Callback() { + @Override + public void succeeded() { + try { + socket.close(); + } catch (IOException e) { + notifier().error("Failed to close socket after Garbage write succeeded", e); + } + } - @Override - public void failed(Throwable x) { - try { - socket.close(); - } catch (IOException e) { - notifier().error("Failed to close socket after Garbage write failed", e); - } - } - }, - BufferUtil.toBuffer(GARBAGE)); + @Override + public void failed(Throwable x) { + try { + socket.close(); + } catch (IOException e) { + notifier().error("Failed to close socket after Garbage write failed", e); + } + } + }, + BufferUtil.toBuffer(GARBAGE)); } } diff --git a/src/main/java/com/github/tomakehurst/wiremock/jetty11/Jetty11HttpServer.java b/src/main/java/com/github/tomakehurst/wiremock/jetty11/Jetty11HttpServer.java index 681c734ef6..b6e67c6816 100644 --- a/src/main/java/com/github/tomakehurst/wiremock/jetty11/Jetty11HttpServer.java +++ b/src/main/java/com/github/tomakehurst/wiremock/jetty11/Jetty11HttpServer.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2019-2023 Thomas Akehurst + * Copyright (C) 2019-2024 Thomas Akehurst * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -15,23 +15,59 @@ */ package com.github.tomakehurst.wiremock.jetty11; +import static com.github.tomakehurst.wiremock.common.Exceptions.throwUnchecked; +import static com.github.tomakehurst.wiremock.common.ResourceUtil.getResource; +import static com.github.tomakehurst.wiremock.core.WireMockApp.ADMIN_CONTEXT_ROOT; import static com.github.tomakehurst.wiremock.jetty11.Jetty11Utils.createHttpConfig; import static com.github.tomakehurst.wiremock.jetty11.SslContexts.buildManInTheMiddleSslContextFactory; +import static java.util.concurrent.Executors.newScheduledThreadPool; +import com.github.tomakehurst.wiremock.common.AsynchronousResponseSettings; +import com.github.tomakehurst.wiremock.common.FileSource; import com.github.tomakehurst.wiremock.common.HttpsSettings; import com.github.tomakehurst.wiremock.common.JettySettings; +import com.github.tomakehurst.wiremock.common.Notifier; import com.github.tomakehurst.wiremock.core.Options; import com.github.tomakehurst.wiremock.http.AdminRequestHandler; +import com.github.tomakehurst.wiremock.http.RequestHandler; import com.github.tomakehurst.wiremock.http.StubRequestHandler; +import com.github.tomakehurst.wiremock.jetty.JettyFaultInjectorFactory; import com.github.tomakehurst.wiremock.jetty.JettyHttpServer; +import com.github.tomakehurst.wiremock.jetty.JettyHttpUtils; +import com.github.tomakehurst.wiremock.jetty.websockets.WebSocketEndpoint; +import com.github.tomakehurst.wiremock.servlet.ContentTypeSettingFilter; +import com.github.tomakehurst.wiremock.servlet.FaultInjectorFactory; +import com.github.tomakehurst.wiremock.servlet.MultipartRequestConfigurer; +import com.github.tomakehurst.wiremock.servlet.NotMatchedServlet; +import com.github.tomakehurst.wiremock.servlet.TrailingSlashFilter; +import com.github.tomakehurst.wiremock.servlet.WireMockHandlerDispatchingServlet; +import jakarta.servlet.DispatcherType; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import java.util.Arrays; +import java.util.EnumSet; +import java.util.Map; +import java.util.Objects; +import java.util.stream.Stream; import org.eclipse.jetty.alpn.server.ALPNServerConnectionFactory; import org.eclipse.jetty.http.HttpVersion; +import org.eclipse.jetty.http.MimeTypes; import org.eclipse.jetty.http2.server.HTTP2CServerConnectionFactory; import org.eclipse.jetty.http2.server.HTTP2ServerConnectionFactory; import org.eclipse.jetty.io.NetworkTrafficListener; +import org.eclipse.jetty.rewrite.handler.RewriteHandler; +import org.eclipse.jetty.rewrite.handler.RewriteRegexRule; import org.eclipse.jetty.server.*; +import org.eclipse.jetty.server.handler.AbstractHandler; import org.eclipse.jetty.server.handler.HandlerCollection; +import org.eclipse.jetty.server.handler.gzip.GzipHandler; +import org.eclipse.jetty.servlet.DefaultServlet; +import org.eclipse.jetty.servlet.FilterHolder; +import org.eclipse.jetty.servlet.ServletContextHandler; +import org.eclipse.jetty.servlet.ServletHolder; +import org.eclipse.jetty.servlets.CrossOriginFilter; import org.eclipse.jetty.util.ssl.SslContextFactory; +import org.eclipse.jetty.websocket.jakarta.server.config.JakartaWebSocketServletContainerInitializer; public class Jetty11HttpServer extends JettyHttpServer { @@ -50,16 +86,17 @@ protected ServerConnector createHttpConnector( HttpConfiguration httpConfig = createHttpConfig(jettySettings); - HTTP2CServerConnectionFactory h2c = new HTTP2CServerConnectionFactory(httpConfig); + ConnectionFactory[] connectionFactories = + Stream.of( + new HttpConnectionFactory(httpConfig), + options.getHttp2PlainDisabled() + ? null + : new HTTP2CServerConnectionFactory(httpConfig)) + .filter(Objects::nonNull) + .toArray(ConnectionFactory[]::new); return Jetty11Utils.createServerConnector( - jettyServer, - bindAddress, - jettySettings, - port, - listener, - new HttpConnectionFactory(httpConfig), - h2c); + jettyServer, bindAddress, jettySettings, port, listener, connectionFactories); } @Override @@ -68,26 +105,37 @@ protected ServerConnector createHttpsConnector( HttpsSettings httpsSettings, JettySettings jettySettings, NetworkTrafficListener listener) { - SslContextFactory.Server http2SslContextFactory = - SslContexts.buildHttp2SslContextFactory(httpsSettings); HttpConfiguration httpConfig = createHttpConfig(jettySettings); - HttpConnectionFactory http = new HttpConnectionFactory(httpConfig); - HTTP2ServerConnectionFactory h2 = new HTTP2ServerConnectionFactory(httpConfig); - ConnectionFactory[] connectionFactories; - try { - ALPNServerConnectionFactory alpn = new ALPNServerConnectionFactory(); - SslConnectionFactory ssl = - new SslConnectionFactory(http2SslContextFactory, alpn.getProtocol()); + if (!options.getHttp2TlsDisabled()) { + + SslContextFactory.Server http2SslContextFactory = + SslContexts.buildHttp2SslContextFactory(httpsSettings); + + HttpConnectionFactory http = new HttpConnectionFactory(httpConfig); + HTTP2ServerConnectionFactory h2 = new HTTP2ServerConnectionFactory(httpConfig); + + try { + ALPNServerConnectionFactory alpn = new ALPNServerConnectionFactory(); + + SslConnectionFactory ssl = + new SslConnectionFactory(http2SslContextFactory, alpn.getProtocol()); - connectionFactories = new ConnectionFactory[] {ssl, alpn, h2, http}; - } catch (IllegalStateException e) { - SslConnectionFactory ssl = - new SslConnectionFactory(http2SslContextFactory, http.getProtocol()); + connectionFactories = new ConnectionFactory[] {ssl, alpn, h2, http}; + } catch (IllegalStateException e) { + SslConnectionFactory ssl = + new SslConnectionFactory(http2SslContextFactory, http.getProtocol()); + connectionFactories = new ConnectionFactory[] {ssl, http}; + } + } else { + final SslContextFactory.Server sslContextFactory = + SslContexts.buildHttp1_1SslContextFactory(httpsSettings); + final SslConnectionFactory ssl = new SslConnectionFactory(sslContextFactory, "http/1.1"); + final HttpConnectionFactory http = new HttpConnectionFactory(httpConfig); connectionFactories = new ConnectionFactory[] {ssl, http}; } @@ -100,22 +148,6 @@ protected ServerConnector createHttpsConnector( connectionFactories); } - @Override - protected HandlerCollection createHandler( - Options options, - AdminRequestHandler adminRequestHandler, - StubRequestHandler stubRequestHandler) { - HandlerCollection handler = - super.createHandler(options, adminRequestHandler, stubRequestHandler); - - if (options.browserProxySettings().enabled()) { - handler.prependHandler(new HttpsProxyDetectingHandler(mitmProxyConnector)); - handler.prependHandler(new ManInTheMiddleSslConnectHandler(mitmProxyConnector)); - } - - return handler; - } - @Override protected void applyAdditionalServerConfiguration(Server jettyServer, Options options) { if (options.browserProxySettings().enabled()) { @@ -159,4 +191,259 @@ actual request (this is how curl 7.64.1 behaves!). Neither jettyServer.addConnector(mitmProxyConnector); } } + + @Override + protected Handler createHandler( + Options options, + AdminRequestHandler adminRequestHandler, + StubRequestHandler stubRequestHandler) { + Notifier notifier = options.notifier(); + ServletContextHandler adminContext = addAdminContext(adminRequestHandler, notifier); + ServletContextHandler mockServiceContext = + addMockServiceContext( + stubRequestHandler, + options.filesRoot(), + options.getAsynchronousResponseSettings(), + options.getChunkedEncodingPolicy(), + options.getStubCorsEnabled(), + options.browserProxySettings().enabled(), + notifier); + + HandlerCollection handlers = new HandlerCollection(); + AbstractHandler asyncTimeoutSettingHandler = + new AbstractHandler() { + @Override + public void handle( + final String target, + final Request baseRequest, + final HttpServletRequest request, + final HttpServletResponse response) { + baseRequest.getHttpChannel().getState().setTimeout(options.timeout()); + } + }; + handlers.setHandlers( + Stream.concat( + Arrays.stream(extensionHandlers()), + Stream.of(adminContext, asyncTimeoutSettingHandler)) + .toArray(Handler[]::new)); + + // wiremock-gui + // We prepend the rewrite handle for the wiremock gui web app to make sure that we route the + // requests properly + final RewriteHandler rewriteHandler = webAppRewriteContext(adminContext); + handlers.prependHandler(rewriteHandler); + + if (options.getGzipDisabled()) { + handlers.addHandler(mockServiceContext); + } else { + addGZipHandler(mockServiceContext, handlers); + } + + if (options.browserProxySettings().enabled()) { + handlers.prependHandler(new HttpsProxyDetectingHandler(mitmProxyConnector)); + handlers.prependHandler(new ManInTheMiddleSslConnectHandler(mitmProxyConnector)); + } + + return handlers; + } + + private ServletContextHandler addMockServiceContext( + StubRequestHandler stubRequestHandler, + FileSource fileSource, + AsynchronousResponseSettings asynchronousResponseSettings, + Options.ChunkedEncodingPolicy chunkedEncodingPolicy, + boolean stubCorsEnabled, + boolean browserProxyingEnabled, + Notifier notifier) { + ServletContextHandler mockServiceContext = new ServletContextHandler(jettyServer, "/"); + + decorateMockServiceContextBeforeConfig(mockServiceContext); + + mockServiceContext.setInitParameter("org.eclipse.jetty.servlet.Default.maxCacheSize", "0"); + mockServiceContext.setInitParameter( + "org.eclipse.jetty.servlet.Default.resourceBase", fileSource.getPath()); + mockServiceContext.setInitParameter("org.eclipse.jetty.servlet.Default.dirAllowed", "false"); + + mockServiceContext.addServlet(DefaultServlet.class, FILES_URL_MATCH); + + final Jetty11HttpUtils utils = new Jetty11HttpUtils(); + mockServiceContext.setAttribute(JettyHttpUtils.class.getName(), utils); + + mockServiceContext.setAttribute( + JettyFaultInjectorFactory.class.getName(), new JettyFaultInjectorFactory(utils)); + mockServiceContext.setAttribute(StubRequestHandler.class.getName(), stubRequestHandler); + mockServiceContext.setAttribute(Notifier.KEY, notifier); + mockServiceContext.setAttribute( + Options.ChunkedEncodingPolicy.class.getName(), chunkedEncodingPolicy); + mockServiceContext.setAttribute("browserProxyingEnabled", browserProxyingEnabled); + ServletHolder servletHolder = + mockServiceContext.addServlet(WireMockHandlerDispatchingServlet.class, "/"); + servletHolder.setInitOrder(1); + servletHolder.setInitParameter( + RequestHandler.HANDLER_CLASS_KEY, StubRequestHandler.class.getName()); + servletHolder.setInitParameter( + FaultInjectorFactory.INJECTOR_CLASS_KEY, JettyFaultInjectorFactory.class.getName()); + servletHolder.setInitParameter( + WireMockHandlerDispatchingServlet.SHOULD_FORWARD_TO_FILES_CONTEXT, "true"); + + if (asynchronousResponseSettings.isEnabled()) { + scheduledExecutorService = newScheduledThreadPool(asynchronousResponseSettings.getThreads()); + mockServiceContext.setAttribute( + WireMockHandlerDispatchingServlet.ASYNCHRONOUS_RESPONSE_EXECUTOR, + scheduledExecutorService); + } + + mockServiceContext.setAttribute( + MultipartRequestConfigurer.KEY, buildMultipartRequestConfigurer()); + + MimeTypes mimeTypes = new MimeTypes(); + mimeTypes.addMimeMapping("json", "application/json"); + mimeTypes.addMimeMapping("html", "text/html"); + mimeTypes.addMimeMapping("xml", "application/xml"); + mimeTypes.addMimeMapping("txt", "text/plain"); + mockServiceContext.setMimeTypes(mimeTypes); + mockServiceContext.setWelcomeFiles( + new String[] {"index.json", "index.html", "index.xml", "index.txt"}); + + NotFoundHandler errorHandler = new NotFoundHandler(mockServiceContext); + mockServiceContext.setErrorHandler(errorHandler); + + mockServiceContext.addFilter( + ContentTypeSettingFilter.class, FILES_URL_MATCH, EnumSet.of(DispatcherType.FORWARD)); + mockServiceContext.addFilter( + TrailingSlashFilter.class, FILES_URL_MATCH, EnumSet.allOf(DispatcherType.class)); + + if (stubCorsEnabled) { + addCorsFilter(mockServiceContext); + } + + decorateMockServiceContextAfterConfig(mockServiceContext); + + return mockServiceContext; + } + + protected void decorateMockServiceContextBeforeConfig(ServletContextHandler mockServiceContext) {} + + protected void decorateMockServiceContextAfterConfig(ServletContextHandler mockServiceContext) {} + + private ServletContextHandler addAdminContext( + AdminRequestHandler adminRequestHandler, Notifier notifier) { + ServletContextHandler adminContext = new ServletContextHandler(jettyServer, ADMIN_CONTEXT_ROOT); + + decorateAdminServiceContextBeforeConfig(adminContext); + + adminContext.setInitParameter("org.eclipse.jetty.servlet.Default.maxCacheSize", "0"); + + String javaVendor = System.getProperty("java.vendor"); + if (javaVendor != null && javaVendor.toLowerCase().contains("android")) { + // Special case for Android, fixes IllegalArgumentException("resource assets not found."): + // The Android ClassLoader apparently does not resolve directories. + // Furthermore, lib assets will be merged into a single asset directory when a jar file is + // assimilated into an apk. + // As resources can be addressed like "assets/swagger-ui/index.html", a static path element + // will suffice. + adminContext.setInitParameter("org.eclipse.jetty.servlet.Default.resourceBase", "assets"); + } else { + adminContext.setInitParameter( + "org.eclipse.jetty.servlet.Default.resourceBase", + getResource(JettyHttpServer.class, "assets").toString()); + } + + getResource(JettyHttpServer.class, "assets/swagger-ui/index.html"); + + adminContext.setInitParameter("org.eclipse.jetty.servlet.Default.dirAllowed", "false"); + ServletHolder swaggerUiServletHolder = + adminContext.addServlet(DefaultServlet.class, "/swagger-ui/*"); + swaggerUiServletHolder.setAsyncSupported(false); + adminContext.addServlet(DefaultServlet.class, "/recorder/*"); + + ServletHolder servletHolder = + adminContext.addServlet(WireMockHandlerDispatchingServlet.class, "/"); + servletHolder.setInitParameter( + RequestHandler.HANDLER_CLASS_KEY, AdminRequestHandler.class.getName()); + adminContext.setAttribute(AdminRequestHandler.class.getName(), adminRequestHandler); + adminContext.setAttribute(Notifier.KEY, notifier); + + adminContext.setAttribute(MultipartRequestConfigurer.KEY, buildMultipartRequestConfigurer()); + + adminContext.addServlet(NotMatchedServlet.class, "/not-matched"); + + addCorsFilter(adminContext); + + // wiremock-gui Include into admin context + ServletHolder webapp = adminContext.addServlet(DefaultServlet.class, "/webapp/*"); + webapp.setAsyncSupported(false); + + // wiremock-gui Include websocket into admin context + JakartaWebSocketServletContainerInitializer.configure( + adminContext, + (servletContext, serverContainer) -> { + serverContainer.addEndpoint(WebSocketEndpoint.class); + }); + + decorateAdminServiceContextAfterConfig(adminContext); + + return adminContext; + } + + protected void decorateAdminServiceContextBeforeConfig( + ServletContextHandler adminServiceContext) {} + + protected void decorateAdminServiceContextAfterConfig( + ServletContextHandler adminServiceContext) {} + + // wiremock-gui + /** + * Rewrite web app. We use Angular. We must rewrite every /__admin/webapp path to the index.html + * of our single page application. + * + * @param adminContextHandler admin context handler is used to provide the static file content + * @return the rewrite handler + */ + private RewriteHandler webAppRewriteContext(ServletContextHandler adminContextHandler) { + final RewriteHandler rewrite = new RewriteHandler(); + rewrite.setRewriteRequestURI(true); + rewrite.setRewritePathInfo(true); + + RewriteRegexRule rewriteRule = new RewriteRegexRule(); + rewriteRule.setRegex("/__admin/webapp/(mappings|matched|unmatched|state|files).*"); + rewriteRule.setReplacement("/__admin/webapp/index.html"); + rewrite.addRule(rewriteRule); + + rewrite.setHandler(adminContextHandler); + + return rewrite; + } + + private void addCorsFilter(ServletContextHandler context) { + context.addFilter(buildCorsFilter(), "/*", EnumSet.of(DispatcherType.REQUEST)); + } + + private FilterHolder buildCorsFilter() { + FilterHolder filterHolder = new FilterHolder(CrossOriginFilter.class); + filterHolder.setInitParameters( + Map.of( + "chainPreflight", + "false", + "allowedOrigins", + "*", + "allowedHeaders", + "*", + "allowedMethods", + "OPTIONS,GET,POST,PUT,PATCH,DELETE")); + return filterHolder; + } + + private void addGZipHandler( + ServletContextHandler mockServiceContext, HandlerCollection handlers) { + try { + GzipHandler gzipHandler = new GzipHandler(); + gzipHandler.addIncludedMethods(GZIPPABLE_METHODS); + gzipHandler.setHandler(mockServiceContext); + gzipHandler.setVary(null); + handlers.addHandler(gzipHandler); + } catch (Exception e) { + throwUnchecked(e); + } + } } diff --git a/src/main/java/com/github/tomakehurst/wiremock/jetty/JettyUtils.java b/src/main/java/com/github/tomakehurst/wiremock/jetty11/Jetty11HttpUtils.java similarity index 56% rename from src/main/java/com/github/tomakehurst/wiremock/jetty/JettyUtils.java rename to src/main/java/com/github/tomakehurst/wiremock/jetty11/Jetty11HttpUtils.java index a28b7c8b3c..d7aba0b817 100644 --- a/src/main/java/com/github/tomakehurst/wiremock/jetty/JettyUtils.java +++ b/src/main/java/com/github/tomakehurst/wiremock/jetty11/Jetty11HttpUtils.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2015-2023 Thomas Akehurst + * Copyright (C) 2015-2024 Thomas Akehurst * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -13,51 +13,28 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package com.github.tomakehurst.wiremock.jetty; +package com.github.tomakehurst.wiremock.jetty11; import static com.github.tomakehurst.wiremock.common.Exceptions.throwUnchecked; import static com.github.tomakehurst.wiremock.jetty11.HttpsProxyDetectingHandler.IS_HTTPS_PROXY_REQUEST_ATTRIBUTE; +import com.github.tomakehurst.wiremock.jetty.JettyHttpUtils; import jakarta.servlet.ServletResponse; import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; import jakarta.servlet.http.HttpServletResponseWrapper; import java.net.Socket; import java.nio.channels.SocketChannel; +import org.eclipse.jetty.io.EndPoint; +import org.eclipse.jetty.io.SelectableChannelEndPoint; import org.eclipse.jetty.io.ssl.SslConnection; import org.eclipse.jetty.server.HttpChannel; import org.eclipse.jetty.server.Request; import org.eclipse.jetty.server.Response; -public class JettyUtils { - - private static final boolean IS_JETTY; - - static { - // do the check only once per classloader / execution - IS_JETTY = isClassExist("org.eclipse.jetty.server.Request"); - } - - private JettyUtils() { - // Hide public constructor - } - - public static boolean isJetty() { - return IS_JETTY; - } - - private static boolean isClassExist(String type) { - try { - ClassLoader contextCL = Thread.currentThread().getContextClassLoader(); - ClassLoader loader = contextCL == null ? JettyUtils.class.getClassLoader() : contextCL; - Class.forName(type, false, loader); - return true; - } catch (Exception e) { - return false; - } - } - - public static Response unwrapResponse(HttpServletResponse httpServletResponse) { +public class Jetty11HttpUtils implements JettyHttpUtils { + @Override + public Response unwrapResponse(HttpServletResponse httpServletResponse) { if (httpServletResponse instanceof HttpServletResponseWrapper) { ServletResponse unwrapped = ((HttpServletResponseWrapper) httpServletResponse).getResponse(); return (Response) unwrapped; @@ -66,7 +43,15 @@ public static Response unwrapResponse(HttpServletResponse httpServletResponse) { return (Response) httpServletResponse; } - public static Socket getTlsSocket(Response response) { + @Override + public Socket socket(Response response) { + HttpChannel httpChannel = response.getHttpOutput().getHttpChannel(); + SelectableChannelEndPoint ep = (SelectableChannelEndPoint) httpChannel.getEndPoint(); + return ((SocketChannel) ep.getChannel()).socket(); + } + + @Override + public Socket tlsSocket(Response response) { HttpChannel httpChannel = response.getHttpOutput().getHttpChannel(); SslConnection.DecryptedEndPoint sslEndpoint = (SslConnection.DecryptedEndPoint) httpChannel.getEndPoint(); @@ -80,13 +65,35 @@ public static Socket getTlsSocket(Response response) { } } - public static boolean isBrowserProxyRequest(HttpServletRequest request) { + @Override + public void setStatusWithReason( + int status, String reason, HttpServletResponse httpServletResponse) { + // The Jetty 11 does not implement HttpServletResponse::setStatus and always sets the + // reason as `null`, the workaround using + // org.eclipse.jetty.server.Response::setStatusWithReason + // still works. + if (httpServletResponse instanceof org.eclipse.jetty.server.Response) { + final org.eclipse.jetty.server.Response jettyResponse = + (org.eclipse.jetty.server.Response) httpServletResponse; + jettyResponse.setStatusWithReason(status, reason); + } else { + httpServletResponse.setStatus(status, reason); + } + } + + @Override + public EndPoint unwrapEndPoint(Response jettyResponse) { + return jettyResponse.getHttpOutput().getHttpChannel().getEndPoint(); + } + + @Override + public boolean isBrowserProxyRequest(HttpServletRequest request) { if (request instanceof Request) { - Request jettyRequest = (Request) request; + final Request jettyRequest = (Request) request; return Boolean.TRUE.equals(request.getAttribute(IS_HTTPS_PROXY_REQUEST_ATTRIBUTE)) || "http".equals(jettyRequest.getMetaData().getURI().getScheme()); + } else { + return false; } - - return false; } } diff --git a/src/main/java/com/github/tomakehurst/wiremock/jetty/NotFoundHandler.java b/src/main/java/com/github/tomakehurst/wiremock/jetty11/NotFoundHandler.java similarity index 96% rename from src/main/java/com/github/tomakehurst/wiremock/jetty/NotFoundHandler.java rename to src/main/java/com/github/tomakehurst/wiremock/jetty11/NotFoundHandler.java index dc339ec4d1..3de53c16ba 100644 --- a/src/main/java/com/github/tomakehurst/wiremock/jetty/NotFoundHandler.java +++ b/src/main/java/com/github/tomakehurst/wiremock/jetty11/NotFoundHandler.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2017-2022 Thomas Akehurst + * Copyright (C) 2017-2024 Thomas Akehurst * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package com.github.tomakehurst.wiremock.jetty; +package com.github.tomakehurst.wiremock.jetty11; import static com.github.tomakehurst.wiremock.common.Exceptions.throwUnchecked; diff --git a/src/main/java/com/github/tomakehurst/wiremock/jetty11/SslContexts.java b/src/main/java/com/github/tomakehurst/wiremock/jetty11/SslContexts.java index ef2bedd791..6236bdb700 100644 --- a/src/main/java/com/github/tomakehurst/wiremock/jetty11/SslContexts.java +++ b/src/main/java/com/github/tomakehurst/wiremock/jetty11/SslContexts.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2019-2022 Thomas Akehurst + * Copyright (C) 2019-2024 Thomas Akehurst * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -43,6 +43,15 @@ public static SslContextFactory.Server buildHttp2SslContextFactory(HttpsSettings return sslContextFactory; } + public static SslContextFactory.Server buildHttp1_1SslContextFactory( + HttpsSettings httpsSettings) { + SslContextFactory.Server sslContextFactory = + SslContexts.defaultSslContextFactory(httpsSettings.keyStore()); + sslContextFactory.setKeyManagerPassword(httpsSettings.keyManagerPassword()); + setupClientAuth(sslContextFactory, httpsSettings); + return sslContextFactory; + } + public static SslContextFactory.Server buildManInTheMiddleSslContextFactory( HttpsSettings httpsSettings, BrowserProxySettings browserProxySettings, @@ -118,7 +127,10 @@ private static X509KeyStore toX509KeyStore(KeyStoreSettings browserProxyCaKeySto } private static X509KeyStore buildKeyStore(KeyStoreSettings browserProxyCaKeyStore) - throws KeyStoreException, IOException, NoSuchAlgorithmException, CertificateException, + throws KeyStoreException, + IOException, + NoSuchAlgorithmException, + CertificateException, CertificateGenerationUnsupportedException { final CertificateAuthority certificateAuthority = CertificateAuthority.generateCertificateAuthority(); diff --git a/src/main/java/com/github/tomakehurst/wiremock/junit/DslWrapper.java b/src/main/java/com/github/tomakehurst/wiremock/junit/DslWrapper.java index 364871a06f..ce3eb10b80 100644 --- a/src/main/java/com/github/tomakehurst/wiremock/junit/DslWrapper.java +++ b/src/main/java/com/github/tomakehurst/wiremock/junit/DslWrapper.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2021-2022 Thomas Akehurst + * Copyright (C) 2021-2024 Thomas Akehurst * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -291,6 +291,11 @@ public void removeStub(StubMapping mappingBuilder) { stubbing.removeStub(mappingBuilder); } + @Override + public void removeStub(UUID id) { + stubbing.removeStub(id); + } + @Override public List getStubMappings() { return stubbing.getStubMappings(); diff --git a/src/main/java/com/github/tomakehurst/wiremock/junit/Stubbing.java b/src/main/java/com/github/tomakehurst/wiremock/junit/Stubbing.java index 88bd79821b..720933cf79 100644 --- a/src/main/java/com/github/tomakehurst/wiremock/junit/Stubbing.java +++ b/src/main/java/com/github/tomakehurst/wiremock/junit/Stubbing.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2013-2021 Thomas Akehurst + * Copyright (C) 2013-2024 Thomas Akehurst * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -38,6 +38,8 @@ public interface Stubbing { void removeStub(StubMapping mappingBuilder); + void removeStub(UUID id); + List getStubMappings(); StubMapping getSingleStubMapping(UUID id); diff --git a/src/main/java/com/github/tomakehurst/wiremock/matching/MatchResult.java b/src/main/java/com/github/tomakehurst/wiremock/matching/MatchResult.java index bb8ad54ed2..8d31be0521 100644 --- a/src/main/java/com/github/tomakehurst/wiremock/matching/MatchResult.java +++ b/src/main/java/com/github/tomakehurst/wiremock/matching/MatchResult.java @@ -16,12 +16,12 @@ package com.github.tomakehurst.wiremock.matching; import static java.util.Arrays.asList; -import static java.util.stream.Collectors.toUnmodifiableList; import com.fasterxml.jackson.annotation.JsonCreator; import com.fasterxml.jackson.annotation.JsonIgnore; import com.fasterxml.jackson.annotation.JsonProperty; import com.github.tomakehurst.wiremock.stubbing.SubEvent; +import java.util.ArrayList; import java.util.List; import java.util.Queue; import java.util.concurrent.LinkedBlockingQueue; @@ -44,7 +44,7 @@ protected void appendSubEvent(SubEvent subEvent) { } public List getSubEvents() { - return subEvents.stream().collect(toUnmodifiableList()); + return new ArrayList<>(subEvents); } @JsonCreator diff --git a/src/main/java/com/github/tomakehurst/wiremock/matching/MatchesJsonPathPattern.java b/src/main/java/com/github/tomakehurst/wiremock/matching/MatchesJsonPathPattern.java index 9194c62596..11bbcfaeac 100644 --- a/src/main/java/com/github/tomakehurst/wiremock/matching/MatchesJsonPathPattern.java +++ b/src/main/java/com/github/tomakehurst/wiremock/matching/MatchesJsonPathPattern.java @@ -16,23 +16,33 @@ package com.github.tomakehurst.wiremock.matching; import static com.github.tomakehurst.wiremock.common.LocalNotifier.notifier; +import static com.github.tomakehurst.wiremock.common.RequestCache.Key.keyFor; import static java.util.stream.Collectors.toList; import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.annotation.JsonSerialize; import com.github.tomakehurst.wiremock.common.Json; import com.github.tomakehurst.wiremock.common.ListOrSingle; +import com.github.tomakehurst.wiremock.common.RequestCache; import com.github.tomakehurst.wiremock.stubbing.SubEvent; +import com.jayway.jsonpath.DocumentContext; import com.jayway.jsonpath.JsonPath; import com.jayway.jsonpath.PathNotFoundException; -import java.util.*; +import java.util.Collection; +import java.util.Comparator; +import java.util.List; +import java.util.Map; @JsonSerialize(using = JsonPathPatternJsonSerializer.class) public class MatchesJsonPathPattern extends PathPattern { + private final JsonPath jsonPath; + public MatchesJsonPathPattern( @JsonProperty("matchesJsonPath") String expectedJsonPath, StringValuePattern valuePattern) { super(expectedJsonPath, valuePattern); + jsonPath = JsonPath.compile(expectedJsonPath); } public MatchesJsonPathPattern(String value) { @@ -54,7 +64,7 @@ protected MatchResult isSimpleMatch(String value) { return MatchResult.noMatch(SubEvent.warning(message)); } try { - Object obj = JsonPath.read(value, expectedValue); + Object obj = evaluateJsonPath(value); boolean result; if (obj instanceof Collection) { @@ -104,8 +114,23 @@ protected MatchResult isAdvancedMatch(String value) { .collect(toList()); return matchResults.stream() - .min(Comparator.comparingDouble(MatchResult::getDistance)) - .orElseGet(() -> MatchResult.noMatch(subEvents)); + .filter(MatchResult::isExactMatch) + .findFirst() + .orElse( + new MatchResult(subEvents) { + @Override + public boolean isExactMatch() { + return false; + } + + @Override + public double getDistance() { + return matchResults.stream() + .min(Comparator.comparingDouble(MatchResult::getDistance)) + .map(MatchResult::getDistance) + .orElse(1.0); + } + }); } catch (SubExpressionException e) { return MatchResult.noMatch(SubEvent.warning(e.getMessage())); } @@ -125,7 +150,7 @@ public ListOrSingle getExpressionResult(final String value) { Object obj = null; try { - obj = JsonPath.read(value, expectedValue); + obj = evaluateJsonPath(value); } catch (PathNotFoundException ignored) { } catch (Exception e) { String error; @@ -160,4 +185,20 @@ public ListOrSingle getExpressionResult(final String value) { return expressionResult; } + + private Object evaluateJsonPath(String value) { + if (value == null) { + return null; + } + + final RequestCache requestCache = RequestCache.getCurrent(); + + final DocumentContext documentContext = + requestCache.get( + keyFor(JsonNode.class, "parsedJson", value.hashCode()), () -> JsonPath.parse(value)); + + return requestCache.get( + keyFor(JsonNode.class, "jsonPathResult", expectedValue, value.hashCode()), + () -> documentContext.read(jsonPath)); + } } diff --git a/src/main/java/com/github/tomakehurst/wiremock/matching/MemoizingMatchResult.java b/src/main/java/com/github/tomakehurst/wiremock/matching/MemoizingMatchResult.java index f53817b85e..c1e48e185e 100644 --- a/src/main/java/com/github/tomakehurst/wiremock/matching/MemoizingMatchResult.java +++ b/src/main/java/com/github/tomakehurst/wiremock/matching/MemoizingMatchResult.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2020-2023 Thomas Akehurst + * Copyright (C) 2020-2024 Thomas Akehurst * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -15,14 +15,16 @@ */ package com.github.tomakehurst.wiremock.matching; +import com.github.tomakehurst.wiremock.stubbing.SubEvent; import com.google.common.base.Supplier; import com.google.common.base.Suppliers; +import java.util.List; public class MemoizingMatchResult extends MatchResult { private final Supplier memoizedDistance = Suppliers.memoize( - new Supplier() { + new Supplier<>() { @Override public Double get() { return target.getDistance(); @@ -31,7 +33,7 @@ public Double get() { private final Supplier memoizedExactMatch = Suppliers.memoize( - new Supplier() { + new Supplier<>() { @Override public Boolean get() { return target.isExactMatch(); @@ -53,4 +55,9 @@ public boolean isExactMatch() { public double getDistance() { return memoizedDistance.get(); } + + @Override + public List getSubEvents() { + return target.getSubEvents(); + } } diff --git a/src/main/java/com/github/tomakehurst/wiremock/matching/MultipleMatchMultiValuePattern.java b/src/main/java/com/github/tomakehurst/wiremock/matching/MultipleMatchMultiValuePattern.java index 72d527293d..7754fa3a96 100644 --- a/src/main/java/com/github/tomakehurst/wiremock/matching/MultipleMatchMultiValuePattern.java +++ b/src/main/java/com/github/tomakehurst/wiremock/matching/MultipleMatchMultiValuePattern.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2023 Thomas Akehurst + * Copyright (C) 2023-2024 Thomas Akehurst * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -19,7 +19,6 @@ import com.github.tomakehurst.wiremock.http.MultiValue; import java.util.List; import java.util.stream.Collectors; -import org.apache.commons.lang3.StringUtils; public abstract class MultipleMatchMultiValuePattern extends MultiValuePattern { @@ -42,7 +41,7 @@ public String getName() { */ @Override public String getExpected() { - return StringUtils.EMPTY; + return ""; } @Override @@ -62,6 +61,6 @@ public MatchResult match(MultiValue value) { @JsonIgnore public String getOperator() { - return StringUtils.EMPTY; + return ""; } } diff --git a/src/main/java/com/github/tomakehurst/wiremock/matching/RequestPattern.java b/src/main/java/com/github/tomakehurst/wiremock/matching/RequestPattern.java index 0205497f15..97409abecb 100644 --- a/src/main/java/com/github/tomakehurst/wiremock/matching/RequestPattern.java +++ b/src/main/java/com/github/tomakehurst/wiremock/matching/RequestPattern.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2011-2023 Thomas Akehurst + * Copyright (C) 2011-2024 Thomas Akehurst * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -18,10 +18,10 @@ import static com.github.tomakehurst.wiremock.client.WireMock.anyUrl; import static com.github.tomakehurst.wiremock.common.ContentTypes.AUTHORIZATION; import static com.github.tomakehurst.wiremock.common.ParameterUtils.getFirstNonNull; +import static com.github.tomakehurst.wiremock.common.Strings.isEmpty; import static com.github.tomakehurst.wiremock.matching.RequestMatcherExtension.NEVER; import static com.github.tomakehurst.wiremock.matching.RequestPatternBuilder.newRequestPattern; import static com.github.tomakehurst.wiremock.matching.WeightedMatchResult.weight; -import static java.util.Arrays.asList; import static java.util.stream.Collectors.toList; import com.fasterxml.jackson.annotation.JsonCreator; @@ -39,7 +39,6 @@ import java.util.*; import java.util.function.Function; import java.util.function.Predicate; -import org.apache.commons.lang3.StringUtils; public class RequestPattern implements NamedValueMatcher { @@ -98,27 +97,39 @@ public RequestPattern( new RequestMatcher() { @Override public MatchResult match(Request request) { - List matchResults = - new ArrayList<>( - asList( - weight(schemeMatches(request), 3.0), - weight(hostMatches(request), 10.0), - weight(portMatches(request), 10.0), - weight(RequestPattern.this.url.match(request.getUrl()), 10.0), - weight(RequestPattern.this.method.match(request.getMethod()), 3.0), - weight(allPathParamsMatch(request)), - weight(allHeadersMatchResult(request)), - weight(allQueryParamsMatch(request)), - weight(allFormParamsMatch(request)), - weight(allCookiesMatch(request)), - weight(allBodyPatternsMatch(request)), - weight(allMultipartPatternsMatch(request)))); - - if (hasInlineCustomMatcher) { - matchResults.add(weight(customMatcher.match(request))); + final List requestPartMatchResults = new ArrayList<>(15); + + requestPartMatchResults.add(weight(schemeMatches(request), 3.0)); + requestPartMatchResults.add(weight(hostMatches(request), 10.0)); + requestPartMatchResults.add(weight(portMatches(request), 10.0)); + requestPartMatchResults.add( + weight(RequestPattern.this.url.match(request.getUrl()), 10.0)); + requestPartMatchResults.add( + weight(RequestPattern.this.method.match(request.getMethod()), 3.0)); + + MatchResult matchResult = + new MemoizingMatchResult(MatchResult.aggregateWeighted(requestPartMatchResults)); + + if (!matchResult.isExactMatch()) { + return matchResult; + } + + requestPartMatchResults.add(weight(allPathParamsMatch(request))); + requestPartMatchResults.add(weight(allHeadersMatchResult(request))); + requestPartMatchResults.add(weight(allQueryParamsMatch(request))); + requestPartMatchResults.add(weight(allFormParamsMatch(request))); + requestPartMatchResults.add(weight(allCookiesMatch(request))); + requestPartMatchResults.add(weight(allBodyPatternsMatch(request))); + requestPartMatchResults.add(weight(allMultipartPatternsMatch(request))); + + matchResult = + new MemoizingMatchResult(MatchResult.aggregateWeighted(requestPartMatchResults)); + if (!matchResult.isExactMatch() || !hasInlineCustomMatcher) { + return matchResult; } - return MatchResult.aggregateWeighted(matchResults); + requestPartMatchResults.add(weight(customMatcher.match(request))); + return new MemoizingMatchResult(MatchResult.aggregateWeighted(requestPartMatchResults)); } @Override @@ -233,18 +244,18 @@ public static RequestPattern everything() { } public MatchResult match(Request request, Map customMatchers) { - if (customMatcherDefinition != null) { + final MatchResult standardMatchResult = matcher.match(request); + if (standardMatchResult.isExactMatch() && customMatcherDefinition != null) { RequestMatcherExtension requestMatcher = getFirstNonNull(customMatchers.get(customMatcherDefinition.getName()), NEVER); - MatchResult standardMatchResult = matcher.match(request); MatchResult customMatchResult = requestMatcher.match(request, customMatcherDefinition.getParameters()); return MatchResult.aggregate(standardMatchResult, customMatchResult); } - return matcher.match(request); + return standardMatchResult; } private MatchResult allCookiesMatch(final Request request) { @@ -374,9 +385,7 @@ private MatchResult allBodyPatternsMatch(final Request request) { pattern -> { if (StringValuePattern.class.isAssignableFrom(pattern.getClass())) { String body = - StringUtils.isEmpty(request.getBodyAsString()) - ? null - : request.getBodyAsString(); + isEmpty(request.getBodyAsString()) ? null : request.getBodyAsString(); return pattern.match(body); } diff --git a/src/main/java/com/github/tomakehurst/wiremock/servlet/WarConfiguration.java b/src/main/java/com/github/tomakehurst/wiremock/servlet/WarConfiguration.java index e97efbf7f6..cd35cc669b 100644 --- a/src/main/java/com/github/tomakehurst/wiremock/servlet/WarConfiguration.java +++ b/src/main/java/com/github/tomakehurst/wiremock/servlet/WarConfiguration.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2016-2023 Thomas Akehurst + * Copyright (C) 2016-2024 Thomas Akehurst * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -61,6 +61,16 @@ public boolean getHttpDisabled() { return false; } + @Override + public boolean getHttp2PlainDisabled() { + return false; + } + + @Override + public boolean getHttp2TlsDisabled() { + return false; + } + @Override public HttpsSettings httpsSettings() { return new HttpsSettings.Builder().build(); diff --git a/src/main/java/com/github/tomakehurst/wiremock/servlet/WireMockHandlerDispatchingServlet.java b/src/main/java/com/github/tomakehurst/wiremock/servlet/WireMockHandlerDispatchingServlet.java index f10d026c73..87fc920993 100644 --- a/src/main/java/com/github/tomakehurst/wiremock/servlet/WireMockHandlerDispatchingServlet.java +++ b/src/main/java/com/github/tomakehurst/wiremock/servlet/WireMockHandlerDispatchingServlet.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2011-2023 Thomas Akehurst + * Copyright (C) 2011-2024 Thomas Akehurst * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -34,6 +34,7 @@ import com.github.tomakehurst.wiremock.core.Options; import com.github.tomakehurst.wiremock.core.WireMockApp; import com.github.tomakehurst.wiremock.http.*; +import com.github.tomakehurst.wiremock.jetty.JettyHttpUtils; import com.github.tomakehurst.wiremock.stubbing.ServeEvent; import com.github.tomakehurst.wiremock.verification.LoggedRequest; import jakarta.servlet.*; @@ -66,6 +67,7 @@ public class WireMockHandlerDispatchingServlet extends HttpServlet { private MultipartRequestConfigurer multipartRequestConfigurer; private Options.ChunkedEncodingPolicy chunkedEncodingPolicy; private boolean browserProxyingEnabled; + private JettyHttpUtils utils; @Override public void init(ServletConfig config) { @@ -112,6 +114,8 @@ public void init(ServletConfig config) { browserProxyingEnabled = Boolean.parseBoolean( getFirstNonNull(context.getAttribute("browserProxyingEnabled"), "false").toString()); + + utils = (JettyHttpUtils) context.getAttribute(JettyHttpUtils.class.getName()); } private String getNormalizedMappedUnder(ServletConfig config) { @@ -144,7 +148,11 @@ protected void service( Request request = new WireMockHttpServletRequestAdapter( - httpServletRequest, multipartRequestConfigurer, mappedUnder, browserProxyingEnabled); + httpServletRequest, + multipartRequestConfigurer, + mappedUnder, + browserProxyingEnabled, + utils); ServletHttpResponder responder = new ServletHttpResponder(httpServletRequest, httpServletResponse); @@ -248,17 +256,8 @@ public void applyResponse( if (response.getStatusMessage() == null) { httpServletResponse.setStatus(response.getStatus()); } else { - // The Jetty 11 does not implement HttpServletResponse::setStatus and always sets the - // reason as `null`, the workaround using - // org.eclipse.jetty.server.Response::setStatusWithReason - // still works. - if (httpServletResponse instanceof org.eclipse.jetty.server.Response) { - final org.eclipse.jetty.server.Response jettyResponse = - (org.eclipse.jetty.server.Response) httpServletResponse; - jettyResponse.setStatusWithReason(response.getStatus(), response.getStatusMessage()); - } else { - httpServletResponse.setStatus(response.getStatus(), response.getStatusMessage()); - } + utils.setStatusWithReason( + response.getStatus(), response.getStatusMessage(), httpServletResponse); } for (HttpHeader header : response.getHeaders().all()) { diff --git a/src/main/java/com/github/tomakehurst/wiremock/servlet/WireMockHttpServletRequestAdapter.java b/src/main/java/com/github/tomakehurst/wiremock/servlet/WireMockHttpServletRequestAdapter.java index 5dd044bad8..3846ae2ff5 100644 --- a/src/main/java/com/github/tomakehurst/wiremock/servlet/WireMockHttpServletRequestAdapter.java +++ b/src/main/java/com/github/tomakehurst/wiremock/servlet/WireMockHttpServletRequestAdapter.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2016-2023 Thomas Akehurst + * Copyright (C) 2016-2024 Thomas Akehurst * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -26,7 +26,7 @@ import com.github.tomakehurst.wiremock.common.Gzip; import com.github.tomakehurst.wiremock.http.*; import com.github.tomakehurst.wiremock.http.multipart.PartParser; -import com.github.tomakehurst.wiremock.jetty.JettyUtils; +import com.github.tomakehurst.wiremock.jetty.JettyHttpUtils; import com.google.common.base.Suppliers; import com.google.common.collect.ImmutableMultimap; import com.google.common.collect.Maps; @@ -50,16 +50,19 @@ public class WireMockHttpServletRequestAdapter implements Request { private final Map cachedFormParameters; private final boolean browserProxyingEnabled; private final String urlPrefixToRemove; + private final JettyHttpUtils utils; private Collection cachedMultiparts; public WireMockHttpServletRequestAdapter( HttpServletRequest request, MultipartRequestConfigurer multipartRequestConfigurer, String urlPrefixToRemove, - boolean browserProxyingEnabled) { + boolean browserProxyingEnabled, + JettyHttpUtils utils) { this.request = request; this.urlPrefixToRemove = urlPrefixToRemove; this.browserProxyingEnabled = browserProxyingEnabled; + this.utils = utils; cachedQueryParams = Suppliers.memoize(() -> splitQuery(request.getQueryString())); @@ -263,11 +266,11 @@ public Map formParameters() { @Override public boolean isBrowserProxyRequest() { // Avoid the performance hit if browser proxying is disabled - if (!browserProxyingEnabled || !JettyUtils.isJetty()) { + if (!browserProxyingEnabled || !JettyHttpUtils.isJetty()) { return false; } - return JettyUtils.isBrowserProxyRequest(request); + return utils.isBrowserProxyRequest(request); } @Override diff --git a/src/main/java/com/github/tomakehurst/wiremock/standalone/CommandLineOptions.java b/src/main/java/com/github/tomakehurst/wiremock/standalone/CommandLineOptions.java index 031a4d1770..a969c94e7a 100644 --- a/src/main/java/com/github/tomakehurst/wiremock/standalone/CommandLineOptions.java +++ b/src/main/java/com/github/tomakehurst/wiremock/standalone/CommandLineOptions.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2011-2023 Thomas Akehurst + * Copyright (C) 2011-2024 Thomas Akehurst * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -61,6 +61,7 @@ public class CommandLineOptions implements Options { private static final String HELP = "help"; private static final String VERSION = "version"; + private static final String GUI_VERSION = "gui-version"; private static final String RECORD_MAPPINGS = "record-mappings"; private static final String MATCH_HEADERS = "match-headers"; private static final String PROXY_ALL = "proxy-all"; @@ -69,6 +70,8 @@ public class CommandLineOptions implements Options { private static final String TIMEOUT = "timeout"; private static final String PORT = "port"; private static final String DISABLE_HTTP = "disable-http"; + private static final String DISABLE_HTTP2_PLAIN = "disable-http2-plain"; + private static final String DISABLE_HTTP2_TLS = "disable-http2-tls"; private static final String BIND_ADDRESS = "bind-address"; private static final String HTTPS_PORT = "https-port"; private static final String HTTPS_KEYSTORE = "https-keystore"; @@ -147,6 +150,8 @@ public CommandLineOptions(String... args) { "The port number for the server to listen on (default: 8080). 0 for dynamic port selection.") .withRequiredArg(); optionParser.accepts(DISABLE_HTTP, "Disable the default HTTP listener."); + optionParser.accepts(DISABLE_HTTP2_PLAIN, "Disable HTTP/2 on plain text (HTTP) connections."); + optionParser.accepts(DISABLE_HTTP2_TLS, "Disable HTTP/2 on TLS (HTTPS) connections."); optionParser .accepts( HTTPS_PORT, @@ -529,6 +534,16 @@ public boolean getHttpDisabled() { return optionSet.has(DISABLE_HTTP); } + @Override + public boolean getHttp2PlainDisabled() { + return optionSet.has(DISABLE_HTTP2_PLAIN); + } + + @Override + public boolean getHttp2TlsDisabled() { + return optionSet.has(DISABLE_HTTP2_TLS); + } + public void setActualHttpPort(int port) { actualHttpPort = port; } @@ -771,6 +786,8 @@ public String toString() { map.put(VERSION, Version.getCurrentVersion()); + map.put(GUI_VERSION, Version.getGuiVersion()); + if (actualHttpPort != null) { map.put(PORT, actualHttpPort); } diff --git a/src/main/java/com/github/tomakehurst/wiremock/standalone/RemoteMappingsLoader.java b/src/main/java/com/github/tomakehurst/wiremock/standalone/RemoteMappingsLoader.java index 8bdf835181..b60d6f0088 100644 --- a/src/main/java/com/github/tomakehurst/wiremock/standalone/RemoteMappingsLoader.java +++ b/src/main/java/com/github/tomakehurst/wiremock/standalone/RemoteMappingsLoader.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2016-2023 Thomas Akehurst + * Copyright (C) 2016-2024 Thomas Akehurst * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,9 +16,9 @@ package com.github.tomakehurst.wiremock.standalone; import static com.github.tomakehurst.wiremock.common.AbstractFileSource.byFileExtension; +import static com.github.tomakehurst.wiremock.common.Strings.substringAfterLast; import static com.github.tomakehurst.wiremock.core.WireMockApp.FILES_ROOT; import static com.github.tomakehurst.wiremock.core.WireMockApp.MAPPINGS_ROOT; -import static org.apache.commons.lang3.StringUtils.substringAfterLast; import com.github.tomakehurst.wiremock.client.ResponseDefinitionBuilder; import com.github.tomakehurst.wiremock.client.WireMock; diff --git a/src/main/java/com/github/tomakehurst/wiremock/standalone/WireMockServerRunner.java b/src/main/java/com/github/tomakehurst/wiremock/standalone/WireMockServerRunner.java index 5b84641638..07003d6bee 100644 --- a/src/main/java/com/github/tomakehurst/wiremock/standalone/WireMockServerRunner.java +++ b/src/main/java/com/github/tomakehurst/wiremock/standalone/WireMockServerRunner.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2011-2023 Thomas Akehurst + * Copyright (C) 2011-2024 Thomas Akehurst * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -58,6 +58,7 @@ public void run(String... args) { } if (options.version()) { out.println(Version.getCurrentVersion()); + out.println(Version.getGuiVersion()); return; } diff --git a/src/main/java/com/github/tomakehurst/wiremock/verification/LoggedRequest.java b/src/main/java/com/github/tomakehurst/wiremock/verification/LoggedRequest.java index 9b952cb56e..7b0a8df0b6 100644 --- a/src/main/java/com/github/tomakehurst/wiremock/verification/LoggedRequest.java +++ b/src/main/java/com/github/tomakehurst/wiremock/verification/LoggedRequest.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2011-2023 Thomas Akehurst + * Copyright (C) 2011-2024 Thomas Akehurst * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -17,6 +17,7 @@ import static com.github.tomakehurst.wiremock.common.Encoding.decodeBase64; import static com.github.tomakehurst.wiremock.common.Encoding.encodeBase64; +import static com.github.tomakehurst.wiremock.common.Lazy.lazy; import static com.github.tomakehurst.wiremock.common.ParameterUtils.getFirstNonNull; import static com.github.tomakehurst.wiremock.common.Strings.stringFromBytes; import static com.github.tomakehurst.wiremock.common.Urls.safelyCreateURL; @@ -26,6 +27,7 @@ import com.fasterxml.jackson.annotation.*; import com.github.tomakehurst.wiremock.common.Dates; import com.github.tomakehurst.wiremock.common.Json; +import com.github.tomakehurst.wiremock.common.Lazy; import com.github.tomakehurst.wiremock.common.Urls; import com.github.tomakehurst.wiremock.http.*; import java.net.URL; @@ -52,6 +54,9 @@ public class LoggedRequest implements Request { private final Collection multiparts; private final String protocol; + private final Lazy lazyBodyAsString; + private final Lazy lazyBodyAsBase64; + public static LoggedRequest createFrom(Request request) { return new LoggedRequest( request.getScheme(), @@ -144,6 +149,9 @@ private LoggedRequest( this.loggedDate = loggedDate; this.multiparts = multiparts; this.protocol = protocol; + + lazyBodyAsString = lazy(() -> stringFromBytes(body, encodingFromContentTypeHeaderOrUtf8())); + lazyBodyAsBase64 = lazy(() -> encodeBase64(body)); } @Override @@ -231,13 +239,13 @@ public byte[] getBody() { @Override @JsonProperty("body") public String getBodyAsString() { - return stringFromBytes(body, encodingFromContentTypeHeaderOrUtf8()); + return lazyBodyAsString.get(); } @Override @JsonProperty("bodyAsBase64") public String getBodyAsBase64() { - return encodeBase64(body); + return lazyBodyAsBase64.get(); } @Override diff --git a/src/main/java/com/github/tomakehurst/wiremock/verification/diff/Diff.java b/src/main/java/com/github/tomakehurst/wiremock/verification/diff/Diff.java index e4c825398a..786df2c447 100644 --- a/src/main/java/com/github/tomakehurst/wiremock/verification/diff/Diff.java +++ b/src/main/java/com/github/tomakehurst/wiremock/verification/diff/Diff.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2016-2023 Thomas Akehurst + * Copyright (C) 2016-2024 Thomas Akehurst * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -18,6 +18,7 @@ import static com.github.tomakehurst.wiremock.client.WireMock.anyUrl; import static com.github.tomakehurst.wiremock.client.WireMock.equalTo; import static com.github.tomakehurst.wiremock.common.ParameterUtils.getFirstNonNull; +import static com.github.tomakehurst.wiremock.common.Strings.isEmpty; import static com.github.tomakehurst.wiremock.verification.diff.SpacerLine.SPACER; import com.github.tomakehurst.wiremock.common.Json; @@ -57,7 +58,6 @@ import java.util.LinkedList; import java.util.List; import java.util.Map; -import org.apache.commons.lang3.StringUtils; public class Diff { @@ -373,9 +373,7 @@ private void addHeaderSectionWithSpacerIfPresent( String operator = generateOperatorStringForMultiValuePattern(headerPattern, ""); String expected = - StringUtils.isEmpty(headerPattern.getExpected()) - ? "" - : ": " + headerPattern.getExpected(); + isEmpty(headerPattern.getExpected()) ? "" : ": " + headerPattern.getExpected(); String printedPatternValue = header.key() + operator + expected; DiffLine section = @@ -438,7 +436,7 @@ private void addBodySectionIfPresent(List> builder) { private static String getExpressionResultString(Body body, PathPattern pathPattern) { String bodyStr = body.asString(); - if (StringUtils.isEmpty(bodyStr)) { + if (isEmpty(bodyStr)) { return null; } else { try { diff --git a/src/main/java/com/github/tomakehurst/wiremock/verification/diff/DiffLine.java b/src/main/java/com/github/tomakehurst/wiremock/verification/diff/DiffLine.java index d22093cb2c..0f67a64fda 100644 --- a/src/main/java/com/github/tomakehurst/wiremock/verification/diff/DiffLine.java +++ b/src/main/java/com/github/tomakehurst/wiremock/verification/diff/DiffLine.java @@ -15,9 +15,10 @@ */ package com.github.tomakehurst.wiremock.verification.diff; +import static com.github.tomakehurst.wiremock.common.Strings.isEmpty; + import com.github.tomakehurst.wiremock.matching.NamedValueMatcher; import com.github.tomakehurst.wiremock.matching.UrlPattern; -import org.apache.commons.lang3.StringUtils; class DiffLine { @@ -55,8 +56,8 @@ protected boolean isExactMatch() { } public String getMessage() { - String message = null; - if (value == null || StringUtils.isEmpty(value.toString())) { + String message; + if (value == null || isEmpty(value.toString())) { message = requestAttribute + " is not present"; } else { message = isExactMatch() ? null : requestAttribute + " does not match"; diff --git a/src/main/java/com/github/tomakehurst/wiremock/verification/diff/PlainTextDiffRenderer.java b/src/main/java/com/github/tomakehurst/wiremock/verification/diff/PlainTextDiffRenderer.java index 6b3cd79a7e..d1cf796f2a 100644 --- a/src/main/java/com/github/tomakehurst/wiremock/verification/diff/PlainTextDiffRenderer.java +++ b/src/main/java/com/github/tomakehurst/wiremock/verification/diff/PlainTextDiffRenderer.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2017-2023 Thomas Akehurst + * Copyright (C) 2017-2024 Thomas Akehurst * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -15,11 +15,10 @@ */ package com.github.tomakehurst.wiremock.verification.diff; +import static com.github.tomakehurst.wiremock.common.Strings.isNotEmpty; import static com.github.tomakehurst.wiremock.common.Strings.normaliseLineBreaks; +import static com.github.tomakehurst.wiremock.common.Strings.rightPad; import static java.lang.System.lineSeparator; -import static org.apache.commons.lang3.StringUtils.isNotEmpty; -import static org.apache.commons.lang3.StringUtils.repeat; -import static org.apache.commons.lang3.StringUtils.rightPad; import com.github.tomakehurst.wiremock.common.Strings; import com.github.tomakehurst.wiremock.matching.RequestMatcherExtension; @@ -70,14 +69,14 @@ private void header(StringBuilder sb) { int middle = getMiddle(); int titleLinePaddingLeft = middle - (titleLine.length() / 2); sb.append(SEPARATOR) - .append(repeat(' ', titleLinePaddingLeft)) + .append(String.valueOf(' ').repeat(titleLinePaddingLeft)) .append(titleLine) .append(SEPARATOR) - .append(repeat(' ', titleLinePaddingLeft)) - .append(repeat('=', titleLine.length())) + .append(String.valueOf(' ').repeat(titleLinePaddingLeft)) + .append(String.valueOf('=').repeat(titleLine.length())) .append(SEPARATOR) .append(SEPARATOR) - .append(repeat('-', consoleWidth)) + .append(String.valueOf('-').repeat(consoleWidth)) .append(SEPARATOR) .append('|') .append(rightPad(" Closest stub", middle)) @@ -85,14 +84,14 @@ private void header(StringBuilder sb) { .append(rightPad(" Request", middle, ' ')) .append('|') .append(SEPARATOR) - .append(repeat('-', consoleWidth)) + .append(String.valueOf('-').repeat(consoleWidth)) .append(SEPARATOR); writeBlankLine(sb); } private void footer(StringBuilder sb) { - sb.append(repeat('-', consoleWidth)).append(SEPARATOR); + sb.append(String.valueOf('-').repeat(consoleWidth)).append(SEPARATOR); } private void writeLine(StringBuilder sb, String left, String right, String message) { diff --git a/src/main/java/org/wiremock/webhooks/Webhooks.java b/src/main/java/org/wiremock/webhooks/Webhooks.java index 954fb54b60..8f004ef2f9 100644 --- a/src/main/java/org/wiremock/webhooks/Webhooks.java +++ b/src/main/java/org/wiremock/webhooks/Webhooks.java @@ -16,9 +16,9 @@ package org.wiremock.webhooks; import static com.github.tomakehurst.wiremock.common.LocalNotifier.notifier; +import static com.github.tomakehurst.wiremock.common.ParameterUtils.getFirstNonNull; import static java.util.concurrent.TimeUnit.MILLISECONDS; import static java.util.stream.Collectors.toList; -import static org.apache.commons.lang3.ObjectUtils.firstNonNull; import com.github.tomakehurst.wiremock.common.*; import com.github.tomakehurst.wiremock.core.Admin; @@ -103,7 +103,7 @@ private void triggerWebhook(ServeEvent serveEvent, Parameters parameters) { String.format( "The target webhook address %s specified by stub %s is denied in WireMock's configuration.", finalDefinition.getUrl(), - firstNonNull( + getFirstNonNull( serveEvent.getStubMapping().getName(), serveEvent.getStubMapping().getId(), ""))); diff --git a/src/main/resources/swagger/schemas/response-definition.yaml b/src/main/resources/swagger/schemas/response-definition.yaml index e6a035f8fe..43bf4c5802 100644 --- a/src/main/resources/swagger/schemas/response-definition.yaml +++ b/src/main/resources/swagger/schemas/response-definition.yaml @@ -13,6 +13,11 @@ allOf: additionalProxyRequestHeaders: type: object description: Extra request headers to send when proxying to another host. + removeProxyRequestHeaders: + type: array + description: Request headers to remove when proxying to another host. + items: + type: string body: type: string description: The response body as a string. Only one of body, base64Body, jsonBody or bodyFileName may be specified. diff --git a/src/main/resources/swagger/wiremock-admin-api.json b/src/main/resources/swagger/wiremock-admin-api.json index c4e46699b1..c36069ce42 100644 --- a/src/main/resources/swagger/wiremock-admin-api.json +++ b/src/main/resources/swagger/wiremock-admin-api.json @@ -2,7 +2,7 @@ "openapi": "3.0.0", "info": { "title": "WireMock", - "version": "3.4.2" + "version": "3.5.2" }, "externalDocs": { "description": "WireMock user documentation", @@ -208,6 +208,13 @@ "type": "object", "description": "Extra request headers to send when proxying to another host." }, + "removeProxyRequestHeaders": { + "type": "array", + "description": "Request headers to remove when proxying to another host.", + "items": { + "type": "string" + } + }, "body": { "type": "string", "description": "The response body as a string. Only one of body, base64Body, jsonBody or bodyFileName may be specified." @@ -521,6 +528,13 @@ "type": "object", "description": "Extra request headers to send when proxying to another host." }, + "removeProxyRequestHeaders": { + "type": "array", + "description": "Request headers to remove when proxying to another host.", + "items": { + "type": "string" + } + }, "body": { "type": "string", "description": "The response body as a string. Only one of body, base64Body, jsonBody or bodyFileName may be specified." @@ -780,6 +794,13 @@ "type": "object", "description": "Extra request headers to send when proxying to another host." }, + "removeProxyRequestHeaders": { + "type": "array", + "description": "Request headers to remove when proxying to another host.", + "items": { + "type": "string" + } + }, "body": { "type": "string", "description": "The response body as a string. Only one of body, base64Body, jsonBody or bodyFileName may be specified." @@ -1125,6 +1146,13 @@ "type": "object", "description": "Extra request headers to send when proxying to another host." }, + "removeProxyRequestHeaders": { + "type": "array", + "description": "Request headers to remove when proxying to another host.", + "items": { + "type": "string" + } + }, "body": { "type": "string", "description": "The response body as a string. Only one of body, base64Body, jsonBody or bodyFileName may be specified." @@ -1400,6 +1428,13 @@ "type": "object", "description": "Extra request headers to send when proxying to another host." }, + "removeProxyRequestHeaders": { + "type": "array", + "description": "Request headers to remove when proxying to another host.", + "items": { + "type": "string" + } + }, "body": { "type": "string", "description": "The response body as a string. Only one of body, base64Body, jsonBody or bodyFileName may be specified." @@ -1659,6 +1694,13 @@ "type": "object", "description": "Extra request headers to send when proxying to another host." }, + "removeProxyRequestHeaders": { + "type": "array", + "description": "Request headers to remove when proxying to another host.", + "items": { + "type": "string" + } + }, "body": { "type": "string", "description": "The response body as a string. Only one of body, base64Body, jsonBody or bodyFileName may be specified." @@ -2085,6 +2127,13 @@ "type": "object", "description": "Extra request headers to send when proxying to another host." }, + "removeProxyRequestHeaders": { + "type": "array", + "description": "Request headers to remove when proxying to another host.", + "items": { + "type": "string" + } + }, "body": { "type": "string", "description": "The response body as a string. Only one of body, base64Body, jsonBody or bodyFileName may be specified." @@ -4145,6 +4194,13 @@ "type": "object", "description": "Extra request headers to send when proxying to another host." }, + "removeProxyRequestHeaders": { + "type": "array", + "description": "Request headers to remove when proxying to another host.", + "items": { + "type": "string" + } + }, "body": { "type": "string", "description": "The response body as a string. Only one of body, base64Body, jsonBody or bodyFileName may be specified." @@ -4768,6 +4824,13 @@ "type": "object", "description": "Extra request headers to send when proxying to another host." }, + "removeProxyRequestHeaders": { + "type": "array", + "description": "Request headers to remove when proxying to another host.", + "items": { + "type": "string" + } + }, "body": { "type": "string", "description": "The response body as a string. Only one of body, base64Body, jsonBody or bodyFileName may be specified." @@ -5327,6 +5390,13 @@ "type": "object", "description": "Extra request headers to send when proxying to another host." }, + "removeProxyRequestHeaders": { + "type": "array", + "description": "Request headers to remove when proxying to another host.", + "items": { + "type": "string" + } + }, "body": { "type": "string", "description": "The response body as a string. Only one of body, base64Body, jsonBody or bodyFileName may be specified." @@ -6164,6 +6234,13 @@ "type": "object", "description": "Extra request headers to send when proxying to another host." }, + "removeProxyRequestHeaders": { + "type": "array", + "description": "Request headers to remove when proxying to another host.", + "items": { + "type": "string" + } + }, "body": { "type": "string", "description": "The response body as a string. Only one of body, base64Body, jsonBody or bodyFileName may be specified." diff --git a/src/main/resources/swagger/wiremock-admin-api.yaml b/src/main/resources/swagger/wiremock-admin-api.yaml index 0f74c88174..996e41c0d8 100644 --- a/src/main/resources/swagger/wiremock-admin-api.yaml +++ b/src/main/resources/swagger/wiremock-admin-api.yaml @@ -2,7 +2,7 @@ openapi: 3.0.0 info: title: WireMock - version: 3.4.2 + version: 3.5.2 externalDocs: description: WireMock user documentation diff --git a/src/main/resources/version.properties b/src/main/resources/version.properties index 350256bf7c..db7597e89e 100644 --- a/src/main/resources/version.properties +++ b/src/main/resources/version.properties @@ -1 +1 @@ -version=3.4.2 \ No newline at end of file +version=3.5.2 \ No newline at end of file diff --git a/src/test/java/benchmarks/JsonPathAdvancedMatchingBenchmark.java b/src/test/java/benchmarks/JsonPathAdvancedMatchingBenchmark.java new file mode 100644 index 0000000000..64d5722fd8 --- /dev/null +++ b/src/test/java/benchmarks/JsonPathAdvancedMatchingBenchmark.java @@ -0,0 +1,143 @@ +/* + * Copyright (C) 2024 Thomas Akehurst + * + * 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 + * + * http://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. + */ +package benchmarks; + +import static com.github.tomakehurst.wiremock.client.WireMock.*; +import static com.github.tomakehurst.wiremock.core.WireMockConfiguration.wireMockConfig; + +import com.github.tomakehurst.wiremock.WireMockServer; +import com.github.tomakehurst.wiremock.testsupport.WireMockResponse; +import com.github.tomakehurst.wiremock.testsupport.WireMockTestClient; +import org.openjdk.jmh.annotations.*; +import org.openjdk.jmh.runner.Runner; +import org.openjdk.jmh.runner.options.Options; +import org.openjdk.jmh.runner.options.OptionsBuilder; + +@BenchmarkMode(Mode.Throughput) +@Warmup(iterations = 2) +@Fork(1) +@Measurement(iterations = 5) +public class JsonPathAdvancedMatchingBenchmark { + + public static final String[] TOPICS = { + "topic-one", + "longer-topic-2", + "very-long-topic-3", + "four", + "five55555555", + "six", + "seven", + "eight", + "nine", + "ten", + "eleven", + "twelve", + "thirteen", + "fourteen", + "fifteen", + "sixteen", + "seventeen", + "eighteen", + "nineteen", + "twenty" + }; + + @State(Scope.Benchmark) + public static class BenchmarkState { + private WireMockServer wm; + private WireMockTestClient client; + + @Setup + public void setup() { + wm = + new WireMockServer( + wireMockConfig().dynamicPort().disableRequestJournal().containerThreads(100)); + wm.start(); + client = new WireMockTestClient(wm.port()); + + for (String topic : TOPICS) { + wm.stubFor( + post("/things") + .withRequestBody(matchingJsonPath("$.[*].topic", equalTo(topic))) + .willReturn(ok(topic))); + } + } + + @TearDown + public void tearDown() { + wm.stop(); + } + } + + @Benchmark + @Threads(50) + public boolean matched(BenchmarkState state) { + final WireMockResponse response = + state.client.postJson("/things", String.format(JSON_TEMPLATE, pickRandom(TOPICS))); + return response.content().equals("very-long-topic-3"); + } + + private static String pickRandom(String[] values) { + return values[(int) (Math.random() * values.length)]; + } + + public static void main(String[] args) throws Exception { + Options opt = + new OptionsBuilder() + .include(JsonPathAdvancedMatchingBenchmark.class.getSimpleName()) + .warmupIterations(2) + .forks(1) + .measurementIterations(5) + .build(); + + new Runner(opt).run(); + } + + static final String JSON_TEMPLATE = + "[\n" + + " {\"topic\": \"A\", \"name\": \"John Doe\", \"age\": 30, \"occupation\": \"Engineer\", \"city\": \"New York\", \"interests\": [\"hiking\", \"reading\", \"cooking\"], \"email\": \"john.doe@example.com\", \"phone\": \"+1234567890\", \"address\": \"123 Main St\", \"member_since\": \"2020-01-15\"},\n" + + " {\"topic\": \"B\", \"name\": \"Alice Smith\", \"age\": 25, \"occupation\": \"Graphic Designer\", \"city\": \"Los Angeles\", \"interests\": [\"painting\", \"yoga\", \"traveling\"], \"email\": \"alice.smith@example.com\", \"phone\": \"+1987654321\", \"address\": \"456 Elm St\", \"member_since\": \"2018-07-20\"},\n" + + " {\"topic\": \"C\", \"name\": \"Michael Johnson\", \"age\": 35, \"occupation\": \"Doctor\", \"city\": \"Chicago\", \"interests\": [\"running\", \"photography\", \"music\"], \"email\": \"michael.johnson@example.com\", \"phone\": \"+1122334455\", \"address\": \"789 Oak St\", \"member_since\": \"2016-03-10\"},\n" + + " {\"topic\": \"D\", \"name\": \"Emily Brown\", \"age\": 28, \"occupation\": \"Teacher\", \"city\": \"Houston\", \"interests\": [\"gardening\", \"volunteering\", \"movies\"], \"email\": \"emily.brown@example.com\", \"phone\": \"+1443322110\", \"address\": \"101 Pine St\", \"member_since\": \"2019-11-05\"},\n" + + " {\"topic\": \"E\", \"name\": \"Christopher Lee\", \"age\": 40, \"occupation\": \"Software Developer\", \"city\": \"San Francisco\", \"interests\": [\"coding\", \"gaming\", \"hiking\"], \"email\": \"chris.lee@example.com\", \"phone\": \"+1555099887\", \"address\": \"234 Maple St\", \"member_since\": \"2015-05-30\"},\n" + + " {\"topic\": \"F\", \"name\": \"Jessica Taylor\", \"age\": 32, \"occupation\": \"Marketing Manager\", \"city\": \"Seattle\", \"interests\": [\"writing\", \"biking\", \"cooking\"], \"email\": \"jessica.taylor@example.com\", \"phone\": \"+1662777999\", \"address\": \"567 Cedar St\", \"member_since\": \"2017-09-12\"},\n" + + " {\"topic\": \"G\", \"name\": \"Daniel Martinez\", \"age\": 45, \"occupation\": \"Architect\", \"city\": \"Miami\", \"interests\": [\"traveling\", \"drawing\", \"surfing\"], \"email\": \"daniel.martinez@example.com\", \"phone\": \"+1777555666\", \"address\": \"890 Walnut St\", \"member_since\": \"2014-02-25\"},\n" + + " {\"topic\": \"H\", \"name\": \"Sarah Wilson\", \"age\": 27, \"occupation\": \"Journalist\", \"city\": \"Boston\", \"interests\": [\"reading\", \"running\", \"photography\"], \"email\": \"sarah.wilson@example.com\", \"phone\": \"+1888444333\", \"address\": \"123 Pineapple St\", \"member_since\": \"2021-02-18\"},\n" + + " {\"topic\": \"%s\", \"name\": \"David Thompson\", \"age\": 33, \"occupation\": \"Financial Analyst\", \"city\": \"Atlanta\", \"interests\": [\"investing\", \"basketball\", \"traveling\"], \"email\": \"david.thompson@example.com\", \"phone\": \"+1999777666\", \"address\": \"456 Peach St\", \"member_since\": \"2013-08-09\"},\n" + + " {\"topic\": \"J\", \"name\": \"Rachel Garcia\", \"age\": 29, \"occupation\": \"Nurse\", \"city\": \"Dallas\", \"interests\": [\"painting\", \"yoga\", \"movies\"], \"email\": \"rachel.garcia@example.com\", \"phone\": \"+1444999888\", \"address\": \"789 Orange St\", \"member_since\": \"2019-04-27\"},\n" + + " {\"topic\": \"K\", \"name\": \"Kevin Nguyen\", \"age\": 31, \"occupation\": \"Entrepreneur\", \"city\": \"Austin\", \"interests\": [\"coding\", \"startups\", \"traveling\"], \"email\": \"kevin.nguyen@example.com\", \"phone\": \"+1222333444\", \"address\": \"101 Lemon St\", \"member_since\": \"2016-11-14\"},\n" + + " {\"topic\": \"L\", \"name\": \"Rebecca Kim\", \"age\": 26, \"occupation\": \"Graphic Designer\", \"city\": \"Denver\", \"interests\": [\"hiking\", \"photography\", \"music\"], \"email\": \"rebecca.kim@example.com\", \"phone\": \"+1666888999\", \"address\": \"234 Berry St\", \"member_since\": \"2020-08-03\"},\n" + + " {\"topic\": \"M\", \"name\": \"Mark Hernandez\", \"age\": 38, \"occupation\": \"Lawyer\", \"city\": \"Phoenix\", \"interests\": [\"reading\", \"tennis\", \"cooking\"], \"email\": \"mark.hernandez@example.com\", \"phone\": \"+1777666555\", \"address\": \"567 Grape St\", \"member_since\": \"2015-12-21\"},\n" + + " {\"topic\": \"N\", \"name\": \"Jennifer White\", \"age\": 34, \"occupation\": \"HR Manager\", \"city\": \"Philadelphia\", \"interests\": [\"painting\", \"running\", \"movies\"], \"email\": \"jennifer.white@example.com\", \"phone\": \"+1444333222\", \"address\": \"890 Cherry St\", \"member_since\": \"2017-06-08\"},\n" + + " {\"topic\": \"O\", \"name\": \"Andrew Brown\", \"age\": 37, \"occupation\": \"Sales Manager\", \"city\": \"San Diego\", \"interests\": [\"golfing\", \"traveling\", \"cooking\"], \"email\": \"andrew.brown@example.com\", \"phone\": \"+1555888777\", \"address\": \"123 Plum St\", \"member_since\": \"2016-01-30\"},\n" + + " {\"topic\": \"P\", \"name\": \"Lauren Rodriguez\", \"age\": 29, \"occupation\": \"Teacher\", \"city\": \"Orlando\", \"interests\": [\"writing\", \"yoga\", \"movies\"], \"email\": \"lauren.rodriguez@example.com\", \"phone\": \"+1666999888\", \"address\": \"456 Pine St\", \"member_since\": \"2018-10-17\"},\n" + + " {\"topic\": \"Q\", \"name\": \"Jonathan Kim\", \"age\": 42, \"occupation\": \"Doctor\", \"city\": \"Portland\", \"interests\": [\"hiking\", \"painting\", \"music\"], \"email\": \"jonathan.kim@example.com\", \"phone\": \"+1222777666\", \"address\": \"789 Olive St\", \"member_since\": \"2014-05-12\"},\n" + + " {\"topic\": \"R\", \"name\": \"Michelle Garcia\", \"age\": 30, \"occupation\": \"Software Engineer\", \"city\": \"San Antonio\", \"interests\": [\"coding\", \"gaming\", \"traveling\"], \"email\": \"michelle.garcia@example.com\", \"phone\": \"+1333444555\", \"address\": \"890 Walnut St\", \"member_since\": \"2019-02-14\"}\n" + + "]\n"; +} + +/* +Last result but one: + +Benchmark Mode Cnt Score Error Units +JsonPathMatchingBenchmark.matched thrpt 5 9326.701 ± 1291.559 ops/s + + +Last result (pre-compiling JSONPath expression): + +Benchmark Mode Cnt Score Error Units +JsonPathMatchingBenchmark.matched thrpt 5 9600.253 ± 463.052 ops/s + */ diff --git a/src/test/java/benchmarks/JsonPathMatchingBenchTest.java b/src/test/java/benchmarks/JsonPathMatchingBenchTest.java new file mode 100644 index 0000000000..7184386752 --- /dev/null +++ b/src/test/java/benchmarks/JsonPathMatchingBenchTest.java @@ -0,0 +1,144 @@ +/* + * Copyright (C) 2024 Thomas Akehurst + * + * 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 + * + * http://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. + */ +package benchmarks; + +import static com.github.tomakehurst.wiremock.client.WireMock.*; +import static com.github.tomakehurst.wiremock.core.WireMockConfiguration.wireMockConfig; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.is; + +import com.github.tomakehurst.wiremock.WireMockServer; +import com.github.tomakehurst.wiremock.testsupport.WireMockResponse; +import com.github.tomakehurst.wiremock.testsupport.WireMockTestClient; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.openjdk.jmh.runner.Runner; +import org.openjdk.jmh.runner.options.Options; +import org.openjdk.jmh.runner.options.OptionsBuilder; + +public class JsonPathMatchingBenchTest { + + public static final String[] TOPICS = { + "topic-one", "longer-topic-2", "very-long-topic-3", "four", "five55555555" + }; + + private WireMockServer wm; + private WireMockTestClient client; + + @BeforeEach + public void setup() { + wm = new WireMockServer(wireMockConfig().dynamicPort()); + wm.start(); + client = new WireMockTestClient(wm.port()); + } + + @Test + public void simple() { + for (String topic : TOPICS) { + wm.stubFor( + post("/things") + .withRequestBody(matchingJsonPath("$.[?(@.topic == '" + topic + "')]")) + .willReturn(ok(topic))); + } + + final WireMockResponse response = + client.postJson("/things", String.format(JSON_TEMPLATE, "topic-one")); + assertThat(response.statusCode(), is(200)); + assertThat(response.content(), is("topic-one")); + } + + @Test + public void advanced() { + for (String topic : TOPICS) { + wm.stubFor( + post("/things") + .withRequestBody(matchingJsonPath("$.[*].topic", equalTo(topic))) + .willReturn(ok(topic))); + } + + final WireMockResponse response = + client.postJson("/things", String.format(JSON_TEMPLATE, "topic-one")); + assertThat(response.statusCode(), is(200)); + assertThat(response.content(), is("topic-one")); + } + + public static void main(String[] args) throws Exception { + Options opt = + new OptionsBuilder() + .include(JsonPathMatchingBenchTest.class.getSimpleName()) + .warmupIterations(3) + .forks(5) + .measurementIterations(10) + .build(); + + new Runner(opt).run(); + } + + static final String JSON_TEMPLATE = + "[\n" + + " {\n" + + " \"topic\": \"%s\",\n" + + " \"key\": {\n" + + " \"orgId\": \"ORG001\",\n" + + " \"productId\": \"1266009\",\n" + + " \"uom\": \"EACH\",\n" + + " \"locationType\": \"STORE\",\n" + + " \"locationId\": \"S001\",\n" + + " \"sellingChannel\": \"dotcom\",\n" + + " \"fulfillmentType\": \"SHIP\"\n" + + " },\n" + + " \"value\": {\n" + + " \"orgId\": \"ORG001\",\n" + + " \"productId\": \"1266009\",\n" + + " \"uom\": \"EACH\",\n" + + " \"locationType\": \"STORE\",\n" + + " \"locationId\": \"S001\",\n" + + " \"sellingChannel\": \"dotcom\",\n" + + " \"fulfillmentType\": \"SHIP\",\n" + + " \"enabled\": true,\n" + + " \"systemProcessingTime\": {\n" + + " \"min\": 2,\n" + + " \"max\": 5\n" + + " },\n" + + " \"transactionalSystemProcessingTime\": {\n" + + " \"additionalProp1\": {\n" + + " \"min\": 2,\n" + + " \"max\": 5\n" + + " },\n" + + " \"additionalProp2\": {\n" + + " \"min\": 2,\n" + + " \"max\": 5\n" + + " },\n" + + " \"additionalProp3\": {\n" + + " \"min\": 2,\n" + + " \"max\": 5\n" + + " }\n" + + " },\n" + + " \"locationProcessingTime\": {\n" + + " \"min\": 2,\n" + + " \"max\": 5\n" + + " },\n" + + " \"safetyStock\": 2,\n" + + " \"shelfLife\": 5,\n" + + " \"temporaryDisableExpirationTime\": \"2018-06-26T23:24:08.255Z\",\n" + + " \"updateUser\": \"THOR\",\n" + + " \"updateTime\": \"2018-06-21T00:00:00Z\"\n" + + " },\n" + + " \"operation\": \"CREATE\",\n" + + " \"isFullyQualifiedTopicName\": false\n" + + " }\n" + + "]"; +} diff --git a/src/test/java/benchmarks/JsonPathSimpleMatchingBenchmark.java b/src/test/java/benchmarks/JsonPathSimpleMatchingBenchmark.java new file mode 100644 index 0000000000..3574c50e45 --- /dev/null +++ b/src/test/java/benchmarks/JsonPathSimpleMatchingBenchmark.java @@ -0,0 +1,130 @@ +/* + * Copyright (C) 2024 Thomas Akehurst + * + * 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 + * + * http://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. + */ +package benchmarks; + +import static com.github.tomakehurst.wiremock.client.WireMock.*; +import static com.github.tomakehurst.wiremock.core.WireMockConfiguration.wireMockConfig; + +import com.github.tomakehurst.wiremock.WireMockServer; +import com.github.tomakehurst.wiremock.testsupport.WireMockResponse; +import com.github.tomakehurst.wiremock.testsupport.WireMockTestClient; +import org.openjdk.jmh.annotations.*; +import org.openjdk.jmh.runner.Runner; +import org.openjdk.jmh.runner.options.Options; +import org.openjdk.jmh.runner.options.OptionsBuilder; + +@BenchmarkMode(Mode.Throughput) +@Warmup(iterations = 2) +@Fork(1) +@Measurement(iterations = 5) +public class JsonPathSimpleMatchingBenchmark { + + public static final String[] TOPICS = { + "topic-one", + "longer-topic-2", + "very-long-topic-3", + "four", + "five55555555", + "six", + "seven", + "eight", + "nine", + "ten", + "eleven", + "twelve", + "thirteen", + "fourteen", + "fifteen", + "sixteen", + "seventeen", + "eighteen", + "nineteen", + "twenty" + }; + + @State(Scope.Benchmark) + public static class BenchmarkState { + private WireMockServer wm; + private WireMockTestClient client; + + @Setup + public void setup() { + wm = + new WireMockServer( + wireMockConfig().dynamicPort().disableRequestJournal().containerThreads(100)); + wm.start(); + client = new WireMockTestClient(wm.port()); + + for (String topic : TOPICS) { + wm.stubFor( + post("/things") + .withRequestBody(matchingJsonPath("$.[?(@.topic == '" + topic + "')]")) + .willReturn(ok(topic))); + } + } + + @TearDown + public void tearDown() { + wm.stop(); + } + } + + @Benchmark + @Threads(50) + public boolean matched(BenchmarkState state) { + final WireMockResponse response = + state.client.postJson("/things", String.format(JSON_TEMPLATE, pickRandom(TOPICS))); + return response.content().equals("very-long-topic-3"); + } + + private static String pickRandom(String[] values) { + return values[(int) (Math.random() * values.length)]; + } + + public static void main(String[] args) throws Exception { + Options opt = + new OptionsBuilder() + .include(JsonPathSimpleMatchingBenchmark.class.getSimpleName()) + .warmupIterations(2) + .forks(1) + .measurementIterations(5) + .build(); + + new Runner(opt).run(); + } + + static final String JSON_TEMPLATE = + "[\n" + + " {\"topic\": \"A\", \"name\": \"John Doe\", \"age\": 30, \"occupation\": \"Engineer\", \"city\": \"New York\", \"interests\": [\"hiking\", \"reading\", \"cooking\"], \"email\": \"john.doe@example.com\", \"phone\": \"+1234567890\", \"address\": \"123 Main St\", \"member_since\": \"2020-01-15\"},\n" + + " {\"topic\": \"B\", \"name\": \"Alice Smith\", \"age\": 25, \"occupation\": \"Graphic Designer\", \"city\": \"Los Angeles\", \"interests\": [\"painting\", \"yoga\", \"traveling\"], \"email\": \"alice.smith@example.com\", \"phone\": \"+1987654321\", \"address\": \"456 Elm St\", \"member_since\": \"2018-07-20\"},\n" + + " {\"topic\": \"C\", \"name\": \"Michael Johnson\", \"age\": 35, \"occupation\": \"Doctor\", \"city\": \"Chicago\", \"interests\": [\"running\", \"photography\", \"music\"], \"email\": \"michael.johnson@example.com\", \"phone\": \"+1122334455\", \"address\": \"789 Oak St\", \"member_since\": \"2016-03-10\"},\n" + + " {\"topic\": \"D\", \"name\": \"Emily Brown\", \"age\": 28, \"occupation\": \"Teacher\", \"city\": \"Houston\", \"interests\": [\"gardening\", \"volunteering\", \"movies\"], \"email\": \"emily.brown@example.com\", \"phone\": \"+1443322110\", \"address\": \"101 Pine St\", \"member_since\": \"2019-11-05\"},\n" + + " {\"topic\": \"E\", \"name\": \"Christopher Lee\", \"age\": 40, \"occupation\": \"Software Developer\", \"city\": \"San Francisco\", \"interests\": [\"coding\", \"gaming\", \"hiking\"], \"email\": \"chris.lee@example.com\", \"phone\": \"+1555099887\", \"address\": \"234 Maple St\", \"member_since\": \"2015-05-30\"},\n" + + " {\"topic\": \"F\", \"name\": \"Jessica Taylor\", \"age\": 32, \"occupation\": \"Marketing Manager\", \"city\": \"Seattle\", \"interests\": [\"writing\", \"biking\", \"cooking\"], \"email\": \"jessica.taylor@example.com\", \"phone\": \"+1662777999\", \"address\": \"567 Cedar St\", \"member_since\": \"2017-09-12\"},\n" + + " {\"topic\": \"G\", \"name\": \"Daniel Martinez\", \"age\": 45, \"occupation\": \"Architect\", \"city\": \"Miami\", \"interests\": [\"traveling\", \"drawing\", \"surfing\"], \"email\": \"daniel.martinez@example.com\", \"phone\": \"+1777555666\", \"address\": \"890 Walnut St\", \"member_since\": \"2014-02-25\"},\n" + + " {\"topic\": \"H\", \"name\": \"Sarah Wilson\", \"age\": 27, \"occupation\": \"Journalist\", \"city\": \"Boston\", \"interests\": [\"reading\", \"running\", \"photography\"], \"email\": \"sarah.wilson@example.com\", \"phone\": \"+1888444333\", \"address\": \"123 Pineapple St\", \"member_since\": \"2021-02-18\"},\n" + + " {\"topic\": \"%s\", \"name\": \"David Thompson\", \"age\": 33, \"occupation\": \"Financial Analyst\", \"city\": \"Atlanta\", \"interests\": [\"investing\", \"basketball\", \"traveling\"], \"email\": \"david.thompson@example.com\", \"phone\": \"+1999777666\", \"address\": \"456 Peach St\", \"member_since\": \"2013-08-09\"},\n" + + " {\"topic\": \"J\", \"name\": \"Rachel Garcia\", \"age\": 29, \"occupation\": \"Nurse\", \"city\": \"Dallas\", \"interests\": [\"painting\", \"yoga\", \"movies\"], \"email\": \"rachel.garcia@example.com\", \"phone\": \"+1444999888\", \"address\": \"789 Orange St\", \"member_since\": \"2019-04-27\"},\n" + + " {\"topic\": \"K\", \"name\": \"Kevin Nguyen\", \"age\": 31, \"occupation\": \"Entrepreneur\", \"city\": \"Austin\", \"interests\": [\"coding\", \"startups\", \"traveling\"], \"email\": \"kevin.nguyen@example.com\", \"phone\": \"+1222333444\", \"address\": \"101 Lemon St\", \"member_since\": \"2016-11-14\"},\n" + + " {\"topic\": \"L\", \"name\": \"Rebecca Kim\", \"age\": 26, \"occupation\": \"Graphic Designer\", \"city\": \"Denver\", \"interests\": [\"hiking\", \"photography\", \"music\"], \"email\": \"rebecca.kim@example.com\", \"phone\": \"+1666888999\", \"address\": \"234 Berry St\", \"member_since\": \"2020-08-03\"},\n" + + " {\"topic\": \"M\", \"name\": \"Mark Hernandez\", \"age\": 38, \"occupation\": \"Lawyer\", \"city\": \"Phoenix\", \"interests\": [\"reading\", \"tennis\", \"cooking\"], \"email\": \"mark.hernandez@example.com\", \"phone\": \"+1777666555\", \"address\": \"567 Grape St\", \"member_since\": \"2015-12-21\"},\n" + + " {\"topic\": \"N\", \"name\": \"Jennifer White\", \"age\": 34, \"occupation\": \"HR Manager\", \"city\": \"Philadelphia\", \"interests\": [\"painting\", \"running\", \"movies\"], \"email\": \"jennifer.white@example.com\", \"phone\": \"+1444333222\", \"address\": \"890 Cherry St\", \"member_since\": \"2017-06-08\"},\n" + + " {\"topic\": \"O\", \"name\": \"Andrew Brown\", \"age\": 37, \"occupation\": \"Sales Manager\", \"city\": \"San Diego\", \"interests\": [\"golfing\", \"traveling\", \"cooking\"], \"email\": \"andrew.brown@example.com\", \"phone\": \"+1555888777\", \"address\": \"123 Plum St\", \"member_since\": \"2016-01-30\"},\n" + + " {\"topic\": \"P\", \"name\": \"Lauren Rodriguez\", \"age\": 29, \"occupation\": \"Teacher\", \"city\": \"Orlando\", \"interests\": [\"writing\", \"yoga\", \"movies\"], \"email\": \"lauren.rodriguez@example.com\", \"phone\": \"+1666999888\", \"address\": \"456 Pine St\", \"member_since\": \"2018-10-17\"},\n" + + " {\"topic\": \"Q\", \"name\": \"Jonathan Kim\", \"age\": 42, \"occupation\": \"Doctor\", \"city\": \"Portland\", \"interests\": [\"hiking\", \"painting\", \"music\"], \"email\": \"jonathan.kim@example.com\", \"phone\": \"+1222777666\", \"address\": \"789 Olive St\", \"member_since\": \"2014-05-12\"},\n" + + " {\"topic\": \"R\", \"name\": \"Michelle Garcia\", \"age\": 30, \"occupation\": \"Software Engineer\", \"city\": \"San Antonio\", \"interests\": [\"coding\", \"gaming\", \"traveling\"], \"email\": \"michelle.garcia@example.com\", \"phone\": \"+1333444555\", \"address\": \"890 Walnut St\", \"member_since\": \"2019-02-14\"}\n" + + "]\n"; +} diff --git a/src/test/java/benchmarks/PathAndMethodMatchingBenchmark.java b/src/test/java/benchmarks/PathAndMethodMatchingBenchmark.java new file mode 100644 index 0000000000..c4ef6053e2 --- /dev/null +++ b/src/test/java/benchmarks/PathAndMethodMatchingBenchmark.java @@ -0,0 +1,79 @@ +/* + * Copyright (C) 2024 Thomas Akehurst + * + * 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 + * + * http://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. + */ +package benchmarks; + +import static com.github.tomakehurst.wiremock.client.WireMock.*; +import static com.github.tomakehurst.wiremock.core.WireMockConfiguration.wireMockConfig; +import static java.util.stream.Collectors.toUnmodifiableList; + +import com.github.tomakehurst.wiremock.WireMockServer; +import com.github.tomakehurst.wiremock.testsupport.WireMockTestClient; +import java.util.List; +import java.util.Objects; +import java.util.UUID; +import java.util.stream.Stream; +import org.openjdk.jmh.annotations.*; + +@BenchmarkMode(Mode.Throughput) +@Warmup(iterations = 2) +@Fork(1) +@Measurement(iterations = 5) +public class PathAndMethodMatchingBenchmark { + + static final List IDS = + Stream.iterate((String) null, ignored -> UUID.randomUUID().toString()) + .filter(Objects::nonNull) + .limit(1000) + .collect(toUnmodifiableList()); + + @State(Scope.Benchmark) + public static class PathAndMethodBenchmarkState { + private WireMockServer wm; + private WireMockTestClient client; + + @Setup + public void setup() { + wm = + new WireMockServer( + wireMockConfig().dynamicPort().disableRequestJournal().containerThreads(100)); + wm.start(); + client = new WireMockTestClient(wm.port()); + + for (String id : IDS) { + wm.stubFor(get("/things/" + id).willReturn(ok("GET " + id))); + wm.stubFor(post("/things/" + id).willReturn(ok("POST " + id))); + } + } + + @TearDown + public void tearDown() { + wm.stop(); + } + } + + @Benchmark + @Threads(50) + public boolean matched(PathAndMethodBenchmarkState state) { + final String id = pickRandom(IDS); + String get = state.client.get("/things/" + id).content(); + String post = state.client.postJson("/things/" + id, "{}").content(); + return get.equals("GET " + id) && post.equals("POST " + id); + } + + private static String pickRandom(List values) { + return values.get((int) (Math.random() * values.size())); + } +} diff --git a/src/test/java/com/github/tomakehurst/wiremock/AdminApiTest.java b/src/test/java/com/github/tomakehurst/wiremock/AdminApiTest.java index d0519a1dc5..9ef085f017 100644 --- a/src/test/java/com/github/tomakehurst/wiremock/AdminApiTest.java +++ b/src/test/java/com/github/tomakehurst/wiremock/AdminApiTest.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2016-2023 Thomas Akehurst + * Copyright (C) 2016-2024 Thomas Akehurst * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -711,7 +711,7 @@ public void returnsBadEntityStatusOnEqualToJsonOperand() { allOf( containsString( "Unexpected character ('(' (code 40)): expected a valid value (JSON String, Number, Array, Object or token 'null', 'true' or 'false')"), - containsString("line: 1, column: 2"))); + containsString("line: 1, column: 1"))); } @Test @@ -1339,7 +1339,8 @@ public void getVersionRequestDefaultsToJson() throws Exception { assertThat(response.firstHeader("Content-Type"), is("application/json")); JSONAssert.assertEquals( "{ \n" - + " \"version\" : \"X.X.X\" \n" + + " \"version\" : \"X.X.X\", \n" + + " \"guiVersion\" : \"X.X.X\" \n" + "}", response.content(), true); diff --git a/src/test/java/com/github/tomakehurst/wiremock/FaultsAcceptanceTest.java b/src/test/java/com/github/tomakehurst/wiremock/FaultsAcceptanceTest.java new file mode 100644 index 0000000000..923b652e89 --- /dev/null +++ b/src/test/java/com/github/tomakehurst/wiremock/FaultsAcceptanceTest.java @@ -0,0 +1,84 @@ +/* + * Copyright (C) 2024 Thomas Akehurst + * + * 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 + * + * http://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. + */ +package com.github.tomakehurst.wiremock; + +import static com.github.tomakehurst.wiremock.client.WireMock.*; +import static com.github.tomakehurst.wiremock.client.WireMock.aResponse; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.instanceOf; +import static org.hamcrest.Matchers.is; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import com.github.tomakehurst.wiremock.http.Fault; +import com.github.tomakehurst.wiremock.testsupport.WireMockResponse; +import org.apache.hc.core5.http.MalformedChunkCodingException; +import org.apache.hc.core5.http.NoHttpResponseException; +import org.junit.jupiter.api.Test; + +public class FaultsAcceptanceTest extends AcceptanceTestBase { + + @Test + public void connectionResetByPeerFault() { + stubFor( + get(urlEqualTo("/connection/reset")) + .willReturn(aResponse().withFault(Fault.CONNECTION_RESET_BY_PEER))); + + RuntimeException runtimeException = + assertThrows(RuntimeException.class, () -> testClient.get("/connection/reset")); + assertThat(runtimeException.getMessage(), is("java.net.SocketException: Connection reset")); + } + + @Test + public void emptyResponseFault() { + stubFor( + get(urlEqualTo("/empty/response")).willReturn(aResponse().withFault(Fault.EMPTY_RESPONSE))); + + getAndAssertUnderlyingExceptionInstanceClass("/empty/response", NoHttpResponseException.class); + } + + @Test + public void malformedResponseChunkFault() { + stubFor( + get(urlEqualTo("/malformed/response")) + .willReturn(aResponse().withFault(Fault.MALFORMED_RESPONSE_CHUNK))); + + getAndAssertUnderlyingExceptionInstanceClass( + "/malformed/response", MalformedChunkCodingException.class); + } + + @Test + public void randomDataOnSocketFault() { + stubFor( + get(urlEqualTo("/random/data")) + .willReturn(aResponse().withFault(Fault.RANDOM_DATA_THEN_CLOSE))); + + getAndAssertUnderlyingExceptionInstanceClass("/random/data", NoHttpResponseException.class); + } + + private void getAndAssertUnderlyingExceptionInstanceClass(String url, Class expectedClass) { + boolean thrown = false; + try { + WireMockResponse response = testClient.get(url); + response.content(); + } catch (Exception e) { + assertThat(e.getCause(), instanceOf(expectedClass)); + thrown = true; + } + + assertTrue(thrown, "No exception was thrown"); + } +} diff --git a/src/test/java/com/github/tomakehurst/wiremock/GzipAcceptanceTest.java b/src/test/java/com/github/tomakehurst/wiremock/GzipAcceptanceTest.java index 3c821ee0c0..7a7c302706 100644 --- a/src/test/java/com/github/tomakehurst/wiremock/GzipAcceptanceTest.java +++ b/src/test/java/com/github/tomakehurst/wiremock/GzipAcceptanceTest.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2015-2023 Thomas Akehurst + * Copyright (C) 2015-2024 Thomas Akehurst * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -18,6 +18,7 @@ import static com.github.tomakehurst.wiremock.client.WireMock.*; import static com.github.tomakehurst.wiremock.common.Gzip.gzip; import static com.github.tomakehurst.wiremock.common.Gzip.unGzipToString; +import static com.github.tomakehurst.wiremock.common.Strings.randomAlphabetic; import static com.github.tomakehurst.wiremock.core.WireMockConfiguration.wireMockConfig; import static com.github.tomakehurst.wiremock.testsupport.TestHttpHeader.withHeader; import static org.hamcrest.MatcherAssert.assertThat; @@ -28,7 +29,6 @@ import com.github.tomakehurst.wiremock.junit5.WireMockExtension; import com.github.tomakehurst.wiremock.testsupport.WireMockResponse; import com.github.tomakehurst.wiremock.testsupport.WireMockTestClient; -import org.apache.commons.lang3.RandomStringUtils; import org.apache.hc.client5.http.entity.GzipCompressingEntity; import org.apache.hc.core5.http.ContentType; import org.apache.hc.core5.http.HttpEntity; @@ -117,9 +117,7 @@ public void init() { public void returnsContentLengthHeaderWhenChunkedEncodingDisabled() { assumeTrue(isNotOldJettyVersion()); - String bodyText = - RandomStringUtils.randomAlphabetic( - 257); // 256 bytes is the minimum size for gzip to be used + String bodyText = randomAlphabetic(257); // 256 bytes is the minimum size for gzip to be used wm.stubFor(get("/gzip-response").willReturn(ok(bodyText))); WireMockResponse response = diff --git a/src/test/java/com/github/tomakehurst/wiremock/HttpsBrowserProxyAcceptanceTest.java b/src/test/java/com/github/tomakehurst/wiremock/HttpsBrowserProxyAcceptanceTest.java index d696d95d77..3deb68277b 100644 --- a/src/test/java/com/github/tomakehurst/wiremock/HttpsBrowserProxyAcceptanceTest.java +++ b/src/test/java/com/github/tomakehurst/wiremock/HttpsBrowserProxyAcceptanceTest.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2020-2023 Thomas Akehurst + * Copyright (C) 2020-2024 Thomas Akehurst * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -66,13 +66,7 @@ import org.apache.hc.client5.http.ssl.NoopHostnameVerifier; import org.apache.hc.client5.http.ssl.SSLConnectionSocketFactory; import org.apache.hc.core5.http.HttpHost; -import org.eclipse.jetty.client.HttpClient; -import org.eclipse.jetty.client.HttpProxy; -import org.eclipse.jetty.client.Origin; -import org.eclipse.jetty.client.ProxyConfiguration; -import org.eclipse.jetty.client.api.ContentResponse; import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.condition.DisabledForJreRange; import org.junit.jupiter.api.condition.JRE; @@ -132,22 +126,6 @@ public void canProxyHttpsInBrowserHttpsProxyMode() throws Exception { assertThat(response.content(), is("Got it")); } - @Test - @Disabled("Jetty doesn't yet support proxying via HTTP2") - public void canProxyHttpsUsingHttp2InBrowserHttpsProxyMode() throws Exception { - - HttpClient httpClient = Http2ClientFactory.create(); - ProxyConfiguration proxyConfig = httpClient.getProxyConfiguration(); - HttpProxy httpProxy = - new HttpProxy(new Origin.Address("localhost", proxy.getHttpsPort()), true); - proxyConfig.addProxy(httpProxy); - - target.stubFor(get(urlEqualTo("/whatever")).willReturn(aResponse().withBody("Got it"))); - - ContentResponse response = httpClient.GET(target.url("/whatever")); - assertThat(response.getContentAsString(), is("Got it")); - } - @Test public void canStubHttpsInBrowserProxyMode() throws Exception { target.stubFor( diff --git a/src/test/java/com/github/tomakehurst/wiremock/LoggedResponseTruncationTest.java b/src/test/java/com/github/tomakehurst/wiremock/LoggedResponseTruncationTest.java index 818d08e2f5..ef6c8b38b6 100644 --- a/src/test/java/com/github/tomakehurst/wiremock/LoggedResponseTruncationTest.java +++ b/src/test/java/com/github/tomakehurst/wiremock/LoggedResponseTruncationTest.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2022 Thomas Akehurst + * Copyright (C) 2022-2024 Thomas Akehurst * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,13 +16,13 @@ package com.github.tomakehurst.wiremock; import static com.github.tomakehurst.wiremock.client.WireMock.*; +import static com.github.tomakehurst.wiremock.common.Strings.randomAlphabetic; import static com.github.tomakehurst.wiremock.core.WireMockConfiguration.wireMockConfig; import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.Matchers.is; import com.github.tomakehurst.wiremock.junit5.WireMockExtension; import com.github.tomakehurst.wiremock.testsupport.WireMockTestClient; -import org.apache.commons.lang3.RandomStringUtils; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.RegisterExtension; @@ -47,7 +47,7 @@ void init() { @Test void includesFullResponseBodyWhenBelowConfiguredThreshold() { - String bigBody = RandomStringUtils.randomAlphabetic(MAX_SIZE - 1); + String bigBody = randomAlphabetic(MAX_SIZE - 1); wm.stubFor(any(anyUrl()).willReturn(ok(bigBody))); client.get("/big"); @@ -56,7 +56,7 @@ void includesFullResponseBodyWhenBelowConfiguredThreshold() { @Test void includesFullResponseBodyWhenAtConfiguredThreshold() { - String bigBody = RandomStringUtils.randomAlphabetic(MAX_SIZE - 1); + String bigBody = randomAlphabetic(MAX_SIZE - 1); wm.stubFor(any(anyUrl()).willReturn(ok(bigBody))); client.get("/big"); @@ -65,7 +65,7 @@ void includesFullResponseBodyWhenAtConfiguredThreshold() { @Test void truncatesResponseBodyWhenOverConfiguredThreshold() { - String bigBody = RandomStringUtils.randomAlphabetic(MAX_SIZE + 1); + String bigBody = randomAlphabetic(MAX_SIZE + 1); wm.stubFor(any(anyUrl()).willReturn(ok(bigBody))); client.get("/big"); diff --git a/src/test/java/com/github/tomakehurst/wiremock/MultipartBodyMatchingAcceptanceTest.java b/src/test/java/com/github/tomakehurst/wiremock/MultipartBodyMatchingAcceptanceTest.java index c3f5361a1e..eec86d6e14 100644 --- a/src/test/java/com/github/tomakehurst/wiremock/MultipartBodyMatchingAcceptanceTest.java +++ b/src/test/java/com/github/tomakehurst/wiremock/MultipartBodyMatchingAcceptanceTest.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2018-2023 Thomas Akehurst + * Copyright (C) 2018-2024 Thomas Akehurst * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,9 +16,9 @@ package com.github.tomakehurst.wiremock; import static com.github.tomakehurst.wiremock.client.WireMock.*; +import static com.github.tomakehurst.wiremock.common.Strings.randomAlphanumeric; import static com.github.tomakehurst.wiremock.testsupport.MultipartBody.part; import static java.util.Collections.singletonList; -import static org.apache.commons.lang3.RandomStringUtils.randomAlphanumeric; import static org.apache.hc.core5.http.ContentType.MULTIPART_FORM_DATA; import static org.apache.hc.core5.http.ContentType.TEXT_PLAIN; import static org.hamcrest.MatcherAssert.assertThat; diff --git a/src/test/java/com/github/tomakehurst/wiremock/RecordingDslAcceptanceTest.java b/src/test/java/com/github/tomakehurst/wiremock/RecordingDslAcceptanceTest.java index de5032cc35..c05f82421c 100644 --- a/src/test/java/com/github/tomakehurst/wiremock/RecordingDslAcceptanceTest.java +++ b/src/test/java/com/github/tomakehurst/wiremock/RecordingDslAcceptanceTest.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2017-2023 Thomas Akehurst + * Copyright (C) 2017-2024 Thomas Akehurst * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -18,10 +18,11 @@ import static com.github.tomakehurst.wiremock.client.WireMock.*; import static com.github.tomakehurst.wiremock.common.ContentTypes.CONTENT_TYPE; import static com.github.tomakehurst.wiremock.common.Gzip.gzip; -import static com.github.tomakehurst.wiremock.common.Strings.DEFAULT_CHARSET; +import static com.github.tomakehurst.wiremock.common.Strings.rightPad; import static com.github.tomakehurst.wiremock.core.WireMockConfiguration.wireMockConfig; import static com.github.tomakehurst.wiremock.testsupport.TestHttpHeader.withHeader; import static com.github.tomakehurst.wiremock.testsupport.WireMatchers.findMappingWithUrl; +import static java.nio.charset.StandardCharsets.UTF_8; import static org.apache.hc.core5.http.ContentType.APPLICATION_OCTET_STREAM; import static org.apache.hc.core5.http.ContentType.TEXT_PLAIN; import static org.hamcrest.MatcherAssert.assertThat; @@ -38,7 +39,6 @@ import com.github.tomakehurst.wiremock.testsupport.WireMockTestClient; import java.io.File; import java.util.List; -import org.apache.commons.lang3.StringUtils; import org.apache.hc.client5.http.entity.GzipCompressingEntity; import org.apache.hc.core5.http.HttpEntity; import org.apache.hc.core5.http.io.entity.StringEntity; @@ -293,7 +293,7 @@ public void recordsIntoPlainTextWhenResponseIsGZipped() { public void recordsIntoPlainBinaryWhenResponseIsGZipped() { proxyingService.startRecording(targetBaseUrl); - byte[] originalBody = "sdkfnslkdjfsjdf".getBytes(DEFAULT_CHARSET); + byte[] originalBody = "sdkfnslkdjfsjdf".getBytes(UTF_8); byte[] gzippedBody = gzip(originalBody); targetService.stubFor( get("/gzipped-response") @@ -337,7 +337,7 @@ public void defaultsToWritingTextResponseFilesOver1Kb() { .willReturn( aResponse() .withHeader(CONTENT_TYPE, "text/plain") - .withBody(StringUtils.rightPad("", 10241, 'a')))); + .withBody(rightPad("", 10241, 'a')))); proxyingService.startRecording(recordSpec().forTarget(targetBaseUrl)); @@ -359,7 +359,7 @@ public void doesNotWriteTextResponseFilesUnder1KbByDefault() { .willReturn( aResponse() .withHeader(CONTENT_TYPE, "text/plain") - .withBody(StringUtils.rightPad("", 10239, 'a')))); + .withBody(rightPad("", 10239, 'a')))); proxyingService.startRecording(recordSpec().forTarget(targetBaseUrl)); diff --git a/src/test/java/com/github/tomakehurst/wiremock/RemoveStubMappingAcceptanceTest.java b/src/test/java/com/github/tomakehurst/wiremock/RemoveStubMappingAcceptanceTest.java index 6da969cdcf..edbef88e79 100644 --- a/src/test/java/com/github/tomakehurst/wiremock/RemoveStubMappingAcceptanceTest.java +++ b/src/test/java/com/github/tomakehurst/wiremock/RemoveStubMappingAcceptanceTest.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2016-2023 Thomas Akehurst + * Copyright (C) 2016-2024 Thomas Akehurst * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -135,6 +135,36 @@ public void removeStubThatDoesNotExists() { assertThat(getMatchingStubCount("/stb-1", "/stb-2"), is(2)); } + @Test + public void removeStubWithUUIDThatExists() { + + UUID id1 = UUID.randomUUID(); + + stubFor(get(urlEqualTo("/stub-1")).withId(id1).willReturn(aResponse().withBody("Stub-1-Body"))); + + assertThat(testClient.get("/stub-1").content(), is("Stub-1-Body")); + + assertThat(getMatchingStubCount("/stub-1", ""), is(1)); + + removeStub(id1); + + assertThat(getMatchingStubCount("/stub-1", ""), is(0)); + } + + @Test + public void removeStubWithUUIDThatDoesNotExists() { + + UUID id1 = UUID.randomUUID(); + + stubFor(get(urlEqualTo("/stb-1")).withId(id1).willReturn(aResponse().withBody("Stb-1-Body"))); + + assertThat(testClient.get("/stb-1").content(), is("Stb-1-Body")); + + removeStub(id1); + + assertThat(getMatchingStubCount("/stb-1", ""), is(0)); + } + private Predicate withAnyOf(final String... urls) { return mapping -> mapping.getRequest().getUrl() != null diff --git a/src/test/java/com/github/tomakehurst/wiremock/RequestFilterAcceptanceTest.java b/src/test/java/com/github/tomakehurst/wiremock/RequestFilterAcceptanceTest.java index 951f796683..03171be1dc 100644 --- a/src/test/java/com/github/tomakehurst/wiremock/RequestFilterAcceptanceTest.java +++ b/src/test/java/com/github/tomakehurst/wiremock/RequestFilterAcceptanceTest.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2019-2023 Thomas Akehurst + * Copyright (C) 2019-2024 Thomas Akehurst * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,6 +16,7 @@ package com.github.tomakehurst.wiremock; import static com.github.tomakehurst.wiremock.client.WireMock.*; +import static com.github.tomakehurst.wiremock.common.Strings.randomAlphabetic; import static com.github.tomakehurst.wiremock.core.WireMockConfiguration.wireMockConfig; import static com.github.tomakehurst.wiremock.testsupport.TestHttpHeader.withHeader; import static org.hamcrest.MatcherAssert.assertThat; @@ -28,7 +29,6 @@ import com.github.tomakehurst.wiremock.testsupport.WireMockResponse; import com.github.tomakehurst.wiremock.testsupport.WireMockTestClient; import java.util.Collections; -import org.apache.commons.lang3.RandomStringUtils; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; @@ -144,7 +144,7 @@ public void wrappedRequestsAreUsedWhenProxying() { @BeforeEach public void init() { - url = "/" + RandomStringUtils.randomAlphabetic(5); + url = "/" + randomAlphabetic(5); } @AfterEach diff --git a/src/test/java/com/github/tomakehurst/wiremock/RequestFilterV2AcceptanceTest.java b/src/test/java/com/github/tomakehurst/wiremock/RequestFilterV2AcceptanceTest.java index 17b61afc4c..a3d2318741 100644 --- a/src/test/java/com/github/tomakehurst/wiremock/RequestFilterV2AcceptanceTest.java +++ b/src/test/java/com/github/tomakehurst/wiremock/RequestFilterV2AcceptanceTest.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2019-2023 Thomas Akehurst + * Copyright (C) 2019-2024 Thomas Akehurst * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,6 +16,7 @@ package com.github.tomakehurst.wiremock; import static com.github.tomakehurst.wiremock.client.WireMock.*; +import static com.github.tomakehurst.wiremock.common.Strings.randomAlphabetic; import static com.github.tomakehurst.wiremock.core.WireMockConfiguration.wireMockConfig; import static com.github.tomakehurst.wiremock.testsupport.TestHttpHeader.withHeader; import static org.hamcrest.MatcherAssert.assertThat; @@ -33,7 +34,6 @@ import java.net.URI; import java.util.Collections; import java.util.Map; -import org.apache.commons.lang3.RandomStringUtils; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; @@ -191,7 +191,7 @@ public String getName() { @BeforeEach public void init() { - url = "/" + RandomStringUtils.randomAlphabetic(5); + url = "/" + randomAlphabetic(5); } @AfterEach diff --git a/src/test/java/com/github/tomakehurst/wiremock/StandaloneAcceptanceTest.java b/src/test/java/com/github/tomakehurst/wiremock/StandaloneAcceptanceTest.java index 890d840632..1deeb69200 100644 --- a/src/test/java/com/github/tomakehurst/wiremock/StandaloneAcceptanceTest.java +++ b/src/test/java/com/github/tomakehurst/wiremock/StandaloneAcceptanceTest.java @@ -32,6 +32,7 @@ import com.github.tomakehurst.wiremock.client.WireMock; import com.github.tomakehurst.wiremock.core.Options; +import com.github.tomakehurst.wiremock.junit5.EnabledIfJettyVersion; import com.github.tomakehurst.wiremock.standalone.MappingFileException; import com.github.tomakehurst.wiremock.standalone.WireMockServerRunner; import com.github.tomakehurst.wiremock.testsupport.MappingJsonSamples; @@ -164,9 +165,14 @@ public void servesFileAsJsonWhenNoFileExtension() { WireMockResponse response = testClient.get("/json/12345"); assertThat(response.statusCode(), is(200)); assertThat(response.content(), is("{ \"key\": \"value\" }")); - assertThat(response.firstHeader("Content-Type"), is("application/json")); + // The "Content-Type" header may include charset, fe "application/json;charset=utf-8" + assertThat(response.firstHeader("Content-Type"), startsWith("application/json")); } + @EnabledIfJettyVersion( + major = 11, + reason = + "Jetty 12 and above always redirects when folder (without trailing slash) is accessed") @Test public void shouldNotSend302WhenPathIsDirAndTrailingSlashNotPresent() { writeFileToFilesDir( @@ -177,6 +183,19 @@ public void shouldNotSend302WhenPathIsDirAndTrailingSlashNotPresent() { assertThat(response.content(), is("{ \"key\": \"index page value\" }")); } + @EnabledIfJettyVersion( + major = 12, + reason = + "Jetty 12 and above always redirects when folder (without trailing slash) is accessed") + @Test + public void shouldSend302WhenPathIsDirAndTrailingSlashNotPresent() { + writeFileToFilesDir( + "json/wire & mock directory/index.json", "{ \"key\": \"index page value\" }"); + startRunner(); + WireMockResponse response = testClient.get("/json/wire%20&%20mock%20directory"); + assertThat(response.statusCode(), is(302)); + } + @Test public void servesJsonIndexFileWhenTrailingSlashPresent() { writeFileToFilesDir("json/23456/index.json", "{ \"key\": \"new value\" }"); @@ -184,7 +203,8 @@ public void servesJsonIndexFileWhenTrailingSlashPresent() { WireMockResponse response = testClient.get("/json/23456/"); assertThat(response.statusCode(), is(200)); assertThat(response.content(), is("{ \"key\": \"new value\" }")); - assertThat(response.firstHeader("Content-Type"), is("application/json")); + // The "Content-Type" header may include charset, fe "application/json;charset=utf-8" + assertThat(response.firstHeader("Content-Type"), startsWith("application/json")); } @Test diff --git a/src/test/java/com/github/tomakehurst/wiremock/StubbingAcceptanceTest.java b/src/test/java/com/github/tomakehurst/wiremock/StubbingAcceptanceTest.java index d1bb531195..ca5c5a0023 100644 --- a/src/test/java/com/github/tomakehurst/wiremock/StubbingAcceptanceTest.java +++ b/src/test/java/com/github/tomakehurst/wiremock/StubbingAcceptanceTest.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2011-2023 Thomas Akehurst + * Copyright (C) 2011-2024 Thomas Akehurst * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -35,11 +35,10 @@ import static org.hamcrest.Matchers.instanceOf; import static org.hamcrest.Matchers.is; import static org.hamcrest.Matchers.notNullValue; -import static org.junit.jupiter.api.Assertions.assertThrows; import static org.junit.jupiter.api.Assertions.assertTrue; import com.github.tomakehurst.wiremock.admin.model.ListStubMappingsResult; -import com.github.tomakehurst.wiremock.http.Fault; +import com.github.tomakehurst.wiremock.junit5.EnabledIfJettyVersion; import com.github.tomakehurst.wiremock.matching.StringValuePattern; import com.github.tomakehurst.wiremock.stubbing.StubMapping; import com.github.tomakehurst.wiremock.testsupport.TestHttpHeader; @@ -54,8 +53,6 @@ import java.util.UUID; import java.util.stream.Stream; import org.apache.hc.core5.http.HttpHeaders; -import org.apache.hc.core5.http.MalformedChunkCodingException; -import org.apache.hc.core5.http.NoHttpResponseException; import org.apache.hc.core5.http.io.entity.ByteArrayEntity; import org.apache.hc.core5.http.io.entity.StringEntity; import org.hamcrest.Description; @@ -442,44 +439,6 @@ public void highPriorityMappingMatchedFirst() { assertThat(testClient.get("/priority/resource").statusCode(), is(200)); } - @Test - public void connectionResetByPeerFault() { - stubFor( - get(urlEqualTo("/connection/reset")) - .willReturn(aResponse().withFault(Fault.CONNECTION_RESET_BY_PEER))); - - RuntimeException runtimeException = - assertThrows(RuntimeException.class, () -> testClient.get("/connection/reset")); - assertThat(runtimeException.getMessage(), is("java.net.SocketException: Connection reset")); - } - - @Test - public void emptyResponseFault() { - stubFor( - get(urlEqualTo("/empty/response")).willReturn(aResponse().withFault(Fault.EMPTY_RESPONSE))); - - getAndAssertUnderlyingExceptionInstanceClass("/empty/response", NoHttpResponseException.class); - } - - @Test - public void malformedResponseChunkFault() { - stubFor( - get(urlEqualTo("/malformed/response")) - .willReturn(aResponse().withFault(Fault.MALFORMED_RESPONSE_CHUNK))); - - getAndAssertUnderlyingExceptionInstanceClass( - "/malformed/response", MalformedChunkCodingException.class); - } - - @Test - public void randomDataOnSocketFault() { - stubFor( - get(urlEqualTo("/random/data")) - .willReturn(aResponse().withFault(Fault.RANDOM_DATA_THEN_CLOSE))); - - getAndAssertUnderlyingExceptionInstanceClass("/random/data", NoHttpResponseException.class); - } - @Test public void matchingUrlsWithEscapeCharacters() { stubFor( @@ -553,6 +512,9 @@ public void stubbingArbitraryMethod() { } @Test + @EnabledIfJettyVersion( + major = 11, + reason = "Jetty 12 and above does not allow setting the status message / reason") public void settingStatusMessage() { stubFor( get(urlEqualTo("/status-message")) diff --git a/src/test/java/com/github/tomakehurst/wiremock/TransferEncodingAcceptanceTest.java b/src/test/java/com/github/tomakehurst/wiremock/TransferEncodingAcceptanceTest.java index 006643998d..c3dc197565 100644 --- a/src/test/java/com/github/tomakehurst/wiremock/TransferEncodingAcceptanceTest.java +++ b/src/test/java/com/github/tomakehurst/wiremock/TransferEncodingAcceptanceTest.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2019-2021 Thomas Akehurst + * Copyright (C) 2019-2024 Thomas Akehurst * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -28,7 +28,6 @@ import com.github.tomakehurst.wiremock.http.HttpClientFactory; import com.github.tomakehurst.wiremock.testsupport.WireMockResponse; import com.github.tomakehurst.wiremock.testsupport.WireMockTestClient; -import org.apache.commons.lang3.StringUtils; import org.apache.hc.client5.http.classic.methods.HttpGet; import org.apache.hc.client5.http.impl.classic.CloseableHttpClient; import org.apache.hc.client5.http.impl.classic.CloseableHttpResponse; @@ -118,7 +117,7 @@ public void sendsSpecifiedContentLengthInResponseWhenChunkedEncodingEnabled() th String path = "/length"; wm.stubFor( get(path) - .willReturn(ok(StringUtils.repeat('a', 1234)).withHeader("Content-Length", "1234"))); + .willReturn(ok(String.valueOf('a').repeat(1234)).withHeader("Content-Length", "1234"))); CloseableHttpClient httpClient = HttpClientFactory.createClient(); HttpGet request = new HttpGet(wm.baseUrl() + path); @@ -134,7 +133,7 @@ public void sendsSpecifiedContentLengthInResponseWhenChunkedEncodingDisabled() t String path = "/length"; wm.stubFor( get(path) - .willReturn(ok(StringUtils.repeat('a', 1234)).withHeader("Content-Length", "1234"))); + .willReturn(ok(String.valueOf('a').repeat(1234)).withHeader("Content-Length", "1234"))); CloseableHttpClient httpClient = HttpClientFactory.createClient(); HttpGet request = new HttpGet(wm.baseUrl() + path); diff --git a/src/test/java/com/github/tomakehurst/wiremock/WireMockClientWithProxyAcceptanceTest.java b/src/test/java/com/github/tomakehurst/wiremock/WireMockClientWithProxyAcceptanceTest.java index e6ca1beea0..a6a44347fe 100644 --- a/src/test/java/com/github/tomakehurst/wiremock/WireMockClientWithProxyAcceptanceTest.java +++ b/src/test/java/com/github/tomakehurst/wiremock/WireMockClientWithProxyAcceptanceTest.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2011-2021 Thomas Akehurst + * Copyright (C) 2011-2024 Thomas Akehurst * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -17,28 +17,29 @@ import static com.github.tomakehurst.wiremock.client.WireMock.*; import static com.github.tomakehurst.wiremock.core.Options.DYNAMIC_PORT; +import static java.net.Proxy.Type.HTTP; import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.Matchers.is; import com.github.tomakehurst.wiremock.client.WireMock; import com.github.tomakehurst.wiremock.testsupport.WireMockTestClient; +import java.net.InetSocketAddress; +import java.net.Proxy; import org.junit.jupiter.api.AfterAll; import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.Test; -import org.littleshoot.proxy.HttpProxyServer; -import org.littleshoot.proxy.impl.DefaultHttpProxyServer; public class WireMockClientWithProxyAcceptanceTest { private static WireMockServer wireMockServer; private static WireMockTestClient testClient; - private static HttpProxyServer proxyServer; + private static Proxy proxyServer; @BeforeAll public static void init() { wireMockServer = new WireMockServer(DYNAMIC_PORT); wireMockServer.start(); - proxyServer = DefaultHttpProxyServer.bootstrap().withPort(0).start(); + proxyServer = new Proxy(HTTP, new InetSocketAddress("localhost", wireMockServer.port())); testClient = new WireMockTestClient(wireMockServer.port()); } @@ -46,17 +47,16 @@ public static void init() { @AfterAll public static void stopServer() { wireMockServer.stop(); - proxyServer.stop(); } @Test - public void supportsProxyingWithTheStaticClient() { + void supportsProxyingWithTheStaticClient() { WireMock.configureFor( "http", "localhost", wireMockServer.port(), - proxyServer.getListenAddress().getHostString(), - proxyServer.getListenAddress().getPort()); + ((InetSocketAddress) proxyServer.address()).getHostString(), + ((InetSocketAddress) proxyServer.address()).getPort()); givenThat(get(urlEqualTo("/my/new/resource")).willReturn(aResponse().withStatus(304))); @@ -64,7 +64,7 @@ public void supportsProxyingWithTheStaticClient() { } @Test - public void supportsProxyingWithTheInstanceClient() { + void supportsProxyingWithTheInstanceClient() { WireMock wireMock = WireMock.create() .scheme("http") @@ -72,8 +72,8 @@ public void supportsProxyingWithTheInstanceClient() { .port(wireMockServer.port()) .urlPathPrefix("") .hostHeader(null) - .proxyHost(proxyServer.getListenAddress().getHostString()) - .proxyPort(proxyServer.getListenAddress().getPort()) + .proxyHost(((InetSocketAddress) proxyServer.address()).getHostString()) + .proxyPort(((InetSocketAddress) proxyServer.address()).getPort()) .build(); wireMock.register( diff --git a/src/test/java/com/github/tomakehurst/wiremock/archunit/GeneralCodingRulesTest.java b/src/test/java/com/github/tomakehurst/wiremock/archunit/GeneralCodingRulesTest.java index 642f09b728..1bea8dcd8e 100644 --- a/src/test/java/com/github/tomakehurst/wiremock/archunit/GeneralCodingRulesTest.java +++ b/src/test/java/com/github/tomakehurst/wiremock/archunit/GeneralCodingRulesTest.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2021 Thomas Akehurst + * Copyright (C) 2021-2024 Thomas Akehurst * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -41,13 +41,14 @@ class GeneralCodingRulesTest { @ArchTest static ArchRule RULE_NO_CLASSES_SHOULD_ACCESS_STANDARD_STREAMS = - noClasses() - .that() - .areNotAssignableTo(ConsoleNotifier.class) - .and() - .areNotAssignableTo(WireMockServerRunner.class) - .should(ACCESS_STANDARD_STREAMS) - .as("classes should not access standard streams"); + freeze( + noClasses() + .that() + .areNotAssignableTo(ConsoleNotifier.class) + .and() + .areNotAssignableTo(WireMockServerRunner.class) + .should(ACCESS_STANDARD_STREAMS) + .as("classes should not access standard streams")); @ArchTest static ArchRule RULE_NO_CLASSES_SHOULD_THROW_GENERIC_EXCEPTIONS = diff --git a/src/test/java/com/github/tomakehurst/wiremock/client/ResponseDefinitionBuilderTest.java b/src/test/java/com/github/tomakehurst/wiremock/client/ResponseDefinitionBuilderTest.java index 2a347bff8e..5baa27c319 100644 --- a/src/test/java/com/github/tomakehurst/wiremock/client/ResponseDefinitionBuilderTest.java +++ b/src/test/java/com/github/tomakehurst/wiremock/client/ResponseDefinitionBuilderTest.java @@ -86,6 +86,7 @@ void proxyResponseDefinitionWithoutProxyInformationIsNotInResponseDefinition() { ResponseDefinitionBuilder.responseDefinition().proxiedFrom("http://my.domain").build(); assertThat(proxyDefinition.getAdditionalProxyRequestHeaders(), nullValue()); + assertThat(proxyDefinition.getRemoveProxyRequestHeaders(), nullValue()); assertThat(proxyDefinition.getProxyUrlPrefixToRemove(), nullValue()); } @@ -98,6 +99,7 @@ void proxyResponseDefinitionWithoutProxyInformationIsNotInResponseDefinitionWith .build(); assertThat(proxyDefinition.getAdditionalProxyRequestHeaders(), nullValue()); + assertThat(proxyDefinition.getRemoveProxyRequestHeaders(), nullValue()); assertThat(proxyDefinition.getProxyUrlPrefixToRemove(), nullValue()); } @@ -110,6 +112,7 @@ void proxyResponseDefinitionWithoutProxyInformationIsNotInResponseDefinitionWith .build(); assertThat(proxyDefinition.getAdditionalProxyRequestHeaders(), nullValue()); + assertThat(proxyDefinition.getRemoveProxyRequestHeaders(), nullValue()); assertThat(proxyDefinition.getProxyUrlPrefixToRemove(), nullValue()); } @@ -119,12 +122,14 @@ void proxyResponseDefinitionWithExtraInformationIsInResponseDefinition() { ResponseDefinitionBuilder.responseDefinition() .proxiedFrom("http://my.domain") .withAdditionalRequestHeader("header", "value") + .withRemoveRequestHeader("header") .withProxyUrlPrefixToRemove("/remove") .build(); assertThat( proxyDefinition.getAdditionalProxyRequestHeaders(), equalTo(new HttpHeaders(List.of(new HttpHeader("header", "value"))))); + assertThat(proxyDefinition.getRemoveProxyRequestHeaders(), equalTo(List.of("header"))); assertThat(proxyDefinition.getProxyUrlPrefixToRemove(), equalTo("/remove")); } @@ -134,6 +139,7 @@ void proxyResponseDefinitionWithExtraInformationIsInResponseDefinitionWithJsonBo ResponseDefinitionBuilder.responseDefinition() .proxiedFrom("http://my.domain") .withAdditionalRequestHeader("header", "value") + .withRemoveRequestHeader("header") .withProxyUrlPrefixToRemove("/remove") .withJsonBody(Json.read("{}", JsonNode.class)) .build(); @@ -141,6 +147,7 @@ void proxyResponseDefinitionWithExtraInformationIsInResponseDefinitionWithJsonBo assertThat( proxyDefinition.getAdditionalProxyRequestHeaders(), equalTo(new HttpHeaders(List.of(new HttpHeader("header", "value"))))); + assertThat(proxyDefinition.getRemoveProxyRequestHeaders(), equalTo(List.of("header"))); assertThat(proxyDefinition.getProxyUrlPrefixToRemove(), equalTo("/remove")); } @@ -150,6 +157,7 @@ void proxyResponseDefinitionWithExtraInformationIsInResponseDefinitionWithBinary ResponseDefinitionBuilder.responseDefinition() .proxiedFrom("http://my.domain") .withAdditionalRequestHeader("header", "value") + .withRemoveRequestHeader("header") .withProxyUrlPrefixToRemove("/remove") .withBody(new byte[] {0x01}) .build(); @@ -157,6 +165,7 @@ void proxyResponseDefinitionWithExtraInformationIsInResponseDefinitionWithBinary assertThat( proxyDefinition.getAdditionalProxyRequestHeaders(), equalTo(new HttpHeaders(List.of(new HttpHeader("header", "value"))))); + assertThat(proxyDefinition.getRemoveProxyRequestHeaders(), equalTo(List.of("header"))); assertThat(proxyDefinition.getProxyUrlPrefixToRemove(), equalTo("/remove")); } diff --git a/src/test/java/com/github/tomakehurst/wiremock/client/WireMockClientWithProxyAcceptanceTest.java b/src/test/java/com/github/tomakehurst/wiremock/client/WireMockClientWithProxyAcceptanceTest.java index e0b50f243e..25ae8836ea 100644 --- a/src/test/java/com/github/tomakehurst/wiremock/client/WireMockClientWithProxyAcceptanceTest.java +++ b/src/test/java/com/github/tomakehurst/wiremock/client/WireMockClientWithProxyAcceptanceTest.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2011-2021 Thomas Akehurst + * Copyright (C) 2011-2024 Thomas Akehurst * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -17,28 +17,29 @@ import static com.github.tomakehurst.wiremock.client.WireMock.*; import static com.github.tomakehurst.wiremock.core.Options.DYNAMIC_PORT; +import static java.net.Proxy.Type.HTTP; import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.Matchers.is; import com.github.tomakehurst.wiremock.WireMockServer; import com.github.tomakehurst.wiremock.testsupport.WireMockTestClient; +import java.net.InetSocketAddress; +import java.net.Proxy; import org.junit.jupiter.api.AfterAll; import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.Test; -import org.littleshoot.proxy.HttpProxyServer; -import org.littleshoot.proxy.impl.DefaultHttpProxyServer; public class WireMockClientWithProxyAcceptanceTest { private static WireMockServer wireMockServer; private static WireMockTestClient testClient; - private static HttpProxyServer proxyServer; + private static Proxy proxyServer; @BeforeAll public static void init() { wireMockServer = new WireMockServer(DYNAMIC_PORT); wireMockServer.start(); - proxyServer = DefaultHttpProxyServer.bootstrap().withPort(0).start(); + proxyServer = new Proxy(HTTP, new InetSocketAddress("localhost", wireMockServer.port())); testClient = new WireMockTestClient(wireMockServer.port()); } @@ -46,17 +47,16 @@ public static void init() { @AfterAll public static void stopServer() { wireMockServer.stop(); - proxyServer.stop(); } @Test - public void supportsProxyingWithTheStaticClient() { + void supportsProxyingWithTheStaticClient() { WireMock.configureFor( "http", "localhost", wireMockServer.port(), - proxyServer.getListenAddress().getHostString(), - proxyServer.getListenAddress().getPort()); + ((InetSocketAddress) proxyServer.address()).getHostString(), + ((InetSocketAddress) proxyServer.address()).getPort()); givenThat(get(urlEqualTo("/my/new/resource")).willReturn(aResponse().withStatus(304))); @@ -64,7 +64,7 @@ public void supportsProxyingWithTheStaticClient() { } @Test - public void supportsProxyingWithTheInstanceClient() { + void supportsProxyingWithTheInstanceClient() { WireMock wireMock = WireMock.create() .scheme("http") @@ -72,8 +72,8 @@ public void supportsProxyingWithTheInstanceClient() { .port(wireMockServer.port()) .urlPathPrefix("") .hostHeader(null) - .proxyHost(proxyServer.getListenAddress().getHostString()) - .proxyPort(proxyServer.getListenAddress().getPort()) + .proxyHost(((InetSocketAddress) proxyServer.address()).getHostString()) + .proxyPort(((InetSocketAddress) proxyServer.address()).getPort()) .build(); wireMock.register( diff --git a/src/test/java/com/github/tomakehurst/wiremock/common/url/PathTemplateTest.java b/src/test/java/com/github/tomakehurst/wiremock/common/url/PathTemplateTest.java index fc7e60011a..ed7f45b65d 100644 --- a/src/test/java/com/github/tomakehurst/wiremock/common/url/PathTemplateTest.java +++ b/src/test/java/com/github/tomakehurst/wiremock/common/url/PathTemplateTest.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2016-2023 Thomas Akehurst + * Copyright (C) 2016-2024 Thomas Akehurst * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -216,7 +216,7 @@ public void checkEquality() { @Test void returnsPathTemplateWithVariablesStrippedOut() { PathTemplate pathTemplate = new PathTemplate("/one/{first}/two/{second}/three"); - assertThat(pathTemplate.withoutVariables(), is("/one//two//three")); + assertThat(pathTemplate.withoutVariables(), is("/one/_/two/_/three")); } @Test diff --git a/src/test/java/com/github/tomakehurst/wiremock/core/ProxyHandlerTest.java b/src/test/java/com/github/tomakehurst/wiremock/core/ProxyHandlerTest.java index 5649050c59..95a63b9667 100644 --- a/src/test/java/com/github/tomakehurst/wiremock/core/ProxyHandlerTest.java +++ b/src/test/java/com/github/tomakehurst/wiremock/core/ProxyHandlerTest.java @@ -1,185 +1,227 @@ +/* + * Copyright (C) 2024 Thomas Akehurst + * + * 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 + * + * http://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. + */ package com.github.tomakehurst.wiremock.core; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + import com.github.tomakehurst.wiremock.admin.model.SingleStubMappingResult; import com.github.tomakehurst.wiremock.http.ResponseDefinition; import com.github.tomakehurst.wiremock.stubbing.StubMapping; -import org.junit.Assert; +import java.util.UUID; import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; -import java.util.UUID; - -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.when; - /** * @author Christopher Holomek */ public class ProxyHandlerTest { - private ProxyHandler proxyHandler; - private static final UUID EXISTING_UUID_IS_PROXY = UUID.randomUUID(); - private static final UUID EXISTING_UUID_IS_PROXY_BINARY = UUID.randomUUID(); - private static final UUID EXISTING_UUID_IS_NO_PROXY = UUID.randomUUID(); - private static final UUID NOT_EXISTING_UUID = UUID.randomUUID(); - - - @BeforeEach - public void before() { - final Admin admin = mock(Admin.class); - - this.proxyHandler = new ProxyHandler(admin); + private ProxyHandler proxyHandler; + private static final UUID EXISTING_UUID_IS_PROXY = UUID.randomUUID(); + private static final UUID EXISTING_UUID_IS_PROXY_BINARY = UUID.randomUUID(); + private static final UUID EXISTING_UUID_IS_NO_PROXY = UUID.randomUUID(); + private static final UUID NOT_EXISTING_UUID = UUID.randomUUID(); - when(admin.getStubMapping(ProxyHandlerTest.EXISTING_UUID_IS_PROXY)).thenReturn(new SingleStubMappingResult( - ProxyHandlerTest.this.createDefaultStubMapping(ProxyHandlerTest.EXISTING_UUID_IS_PROXY, true))); + @BeforeEach + public void before() { + final Admin admin = mock(Admin.class); - when(admin.getStubMapping(ProxyHandlerTest.EXISTING_UUID_IS_PROXY_BINARY)).thenReturn(new SingleStubMappingResult( - ProxyHandlerTest.this.createBinaryStubMapping())); + this.proxyHandler = new ProxyHandler(admin); - when(admin.getStubMapping(ProxyHandlerTest.EXISTING_UUID_IS_NO_PROXY)).thenReturn(new SingleStubMappingResult( - ProxyHandlerTest.this.createDefaultStubMapping(ProxyHandlerTest.EXISTING_UUID_IS_NO_PROXY, false))); + when(admin.getStubMapping(ProxyHandlerTest.EXISTING_UUID_IS_PROXY)) + .thenReturn( + new SingleStubMappingResult( + ProxyHandlerTest.this.createDefaultStubMapping( + ProxyHandlerTest.EXISTING_UUID_IS_PROXY, true))); - when(admin.getStubMapping(ProxyHandlerTest.NOT_EXISTING_UUID)).thenReturn(new SingleStubMappingResult(null)); - } + when(admin.getStubMapping(ProxyHandlerTest.EXISTING_UUID_IS_PROXY_BINARY)) + .thenReturn(new SingleStubMappingResult(ProxyHandlerTest.this.createBinaryStubMapping())); - @Test - public void testClear() { - Assertions.assertTrue(this.proxyHandler.getConfig().isEmpty()); + when(admin.getStubMapping(ProxyHandlerTest.EXISTING_UUID_IS_NO_PROXY)) + .thenReturn( + new SingleStubMappingResult( + ProxyHandlerTest.this.createDefaultStubMapping( + ProxyHandlerTest.EXISTING_UUID_IS_NO_PROXY, false))); - // first we need to disable proxy so that we have something to remove - this.proxyHandler.disableProxyUrl(EXISTING_UUID_IS_PROXY); + when(admin.getStubMapping(ProxyHandlerTest.NOT_EXISTING_UUID)) + .thenReturn(new SingleStubMappingResult(null)); + } - Assertions.assertFalse(this.proxyHandler.getConfig().isEmpty()); + @Test + public void testClear() { + Assertions.assertTrue(this.proxyHandler.getConfig().isEmpty()); - this.proxyHandler.clear(); + // first we need to disable proxy so that we have something to remove + this.proxyHandler.disableProxyUrl(EXISTING_UUID_IS_PROXY); - Assertions.assertTrue(this.proxyHandler.getConfig().isEmpty()); - } + Assertions.assertFalse(this.proxyHandler.getConfig().isEmpty()); + this.proxyHandler.clear(); - @Test - public void testRemoveProxyConfig() { - Assertions.assertTrue(this.proxyHandler.getConfig().isEmpty()); + Assertions.assertTrue(this.proxyHandler.getConfig().isEmpty()); + } - // first we need to disable proxy so that we have something to remove - this.proxyHandler.disableProxyUrl(EXISTING_UUID_IS_PROXY); + @Test + public void testRemoveProxyConfig() { + Assertions.assertTrue(this.proxyHandler.getConfig().isEmpty()); - Assertions.assertFalse(this.proxyHandler.getConfig().isEmpty()); + // first we need to disable proxy so that we have something to remove + this.proxyHandler.disableProxyUrl(EXISTING_UUID_IS_PROXY); - this.proxyHandler.removeProxyConfig(EXISTING_UUID_IS_PROXY); + Assertions.assertFalse(this.proxyHandler.getConfig().isEmpty()); - Assertions.assertFalse(this.proxyHandler.getConfig().containsKey(EXISTING_UUID_IS_PROXY.toString())); - } + this.proxyHandler.removeProxyConfig(EXISTING_UUID_IS_PROXY); - @Test - public void testDisableProxy() { - Assertions.assertTrue(this.proxyHandler.getConfig().isEmpty()); + Assertions.assertFalse( + this.proxyHandler.getConfig().containsKey(EXISTING_UUID_IS_PROXY.toString())); + } - // first we need to disable proxy so that we have something to remove - this.proxyHandler.disableProxyUrl(EXISTING_UUID_IS_PROXY); + @Test + public void testDisableProxy() { + Assertions.assertTrue(this.proxyHandler.getConfig().isEmpty()); - Assertions.assertTrue(this.proxyHandler.getConfig().containsKey(EXISTING_UUID_IS_PROXY.toString())); - } + // first we need to disable proxy so that we have something to remove + this.proxyHandler.disableProxyUrl(EXISTING_UUID_IS_PROXY); - @Test - public void testDisableNoProxyMapping() { - Assertions.assertTrue(this.proxyHandler.getConfig().isEmpty()); + Assertions.assertTrue( + this.proxyHandler.getConfig().containsKey(EXISTING_UUID_IS_PROXY.toString())); + } - // first we need to disable proxy so that we have something to remove - this.proxyHandler.disableProxyUrl(EXISTING_UUID_IS_NO_PROXY); + @Test + public void testDisableNoProxyMapping() { + Assertions.assertTrue(this.proxyHandler.getConfig().isEmpty()); - Assertions.assertFalse(this.proxyHandler.getConfig().containsKey(EXISTING_UUID_IS_NO_PROXY.toString())); - } + // first we need to disable proxy so that we have something to remove + this.proxyHandler.disableProxyUrl(EXISTING_UUID_IS_NO_PROXY); - @Test - public void testDisableNoMapping() { - Assertions.assertTrue(this.proxyHandler.getConfig().isEmpty()); + Assertions.assertFalse( + this.proxyHandler.getConfig().containsKey(EXISTING_UUID_IS_NO_PROXY.toString())); + } - // first we need to disable proxy so that we have something to remove - this.proxyHandler.disableProxyUrl(NOT_EXISTING_UUID); + @Test + public void testDisableNoMapping() { + Assertions.assertTrue(this.proxyHandler.getConfig().isEmpty()); - Assertions.assertFalse(this.proxyHandler.getConfig().containsKey(NOT_EXISTING_UUID.toString())); - } + // first we need to disable proxy so that we have something to remove + this.proxyHandler.disableProxyUrl(NOT_EXISTING_UUID); - @Test - public void testEnableProxy() { - Assertions.assertTrue(this.proxyHandler.getConfig().isEmpty()); + Assertions.assertFalse(this.proxyHandler.getConfig().containsKey(NOT_EXISTING_UUID.toString())); + } - // first we need to disable proxy so that we have something to remove - this.proxyHandler.disableProxyUrl(EXISTING_UUID_IS_PROXY); + @Test + public void testEnableProxy() { + Assertions.assertTrue(this.proxyHandler.getConfig().isEmpty()); - Assertions.assertTrue(this.proxyHandler.getConfig().containsKey(EXISTING_UUID_IS_PROXY.toString())); + // first we need to disable proxy so that we have something to remove + this.proxyHandler.disableProxyUrl(EXISTING_UUID_IS_PROXY); - final String url = this.proxyHandler.getConfig().get(EXISTING_UUID_IS_PROXY.toString()); + Assertions.assertTrue( + this.proxyHandler.getConfig().containsKey(EXISTING_UUID_IS_PROXY.toString())); - Assertions.assertEquals("http://localhost:8080", url); + final String url = this.proxyHandler.getConfig().get(EXISTING_UUID_IS_PROXY.toString()); - this.proxyHandler.enableProxyUrl(EXISTING_UUID_IS_PROXY); + Assertions.assertEquals("http://localhost:8080", url); - Assertions.assertFalse(this.proxyHandler.getConfig().containsKey(EXISTING_UUID_IS_PROXY.toString())); - } + this.proxyHandler.enableProxyUrl(EXISTING_UUID_IS_PROXY); - @Test - public void testEnableNoProxyMapping() { - Assertions.assertTrue(this.proxyHandler.getConfig().isEmpty()); + Assertions.assertFalse( + this.proxyHandler.getConfig().containsKey(EXISTING_UUID_IS_PROXY.toString())); + } - this.proxyHandler.enableProxyUrl(EXISTING_UUID_IS_NO_PROXY); + @Test + public void testEnableNoProxyMapping() { + Assertions.assertTrue(this.proxyHandler.getConfig().isEmpty()); - Assertions.assertFalse(this.proxyHandler.getConfig().containsKey(EXISTING_UUID_IS_NO_PROXY.toString())); - } + this.proxyHandler.enableProxyUrl(EXISTING_UUID_IS_NO_PROXY); - @Test - public void testEnableNoMapping() { - Assertions.assertTrue(this.proxyHandler.getConfig().isEmpty()); + Assertions.assertFalse( + this.proxyHandler.getConfig().containsKey(EXISTING_UUID_IS_NO_PROXY.toString())); + } - this.proxyHandler.enableProxyUrl(NOT_EXISTING_UUID); + @Test + public void testEnableNoMapping() { + Assertions.assertTrue(this.proxyHandler.getConfig().isEmpty()); - Assertions.assertFalse(this.proxyHandler.getConfig().containsKey(NOT_EXISTING_UUID.toString())); - } + this.proxyHandler.enableProxyUrl(NOT_EXISTING_UUID); - @Test - public void testBinary() { - Assertions.assertTrue(this.proxyHandler.getConfig().isEmpty()); + Assertions.assertFalse(this.proxyHandler.getConfig().containsKey(NOT_EXISTING_UUID.toString())); + } - // first we need to disable proxy so that we have something to remove - this.proxyHandler.disableProxyUrl(EXISTING_UUID_IS_PROXY_BINARY); + @Test + public void testBinary() { + Assertions.assertTrue(this.proxyHandler.getConfig().isEmpty()); - Assertions.assertTrue(this.proxyHandler.getConfig().containsKey(EXISTING_UUID_IS_PROXY_BINARY.toString())); + // first we need to disable proxy so that we have something to remove + this.proxyHandler.disableProxyUrl(EXISTING_UUID_IS_PROXY_BINARY); - final String url = this.proxyHandler.getConfig().get(EXISTING_UUID_IS_PROXY_BINARY.toString()); + Assertions.assertTrue( + this.proxyHandler.getConfig().containsKey(EXISTING_UUID_IS_PROXY_BINARY.toString())); - Assertions.assertEquals("http://localhost:8080", url); + final String url = this.proxyHandler.getConfig().get(EXISTING_UUID_IS_PROXY_BINARY.toString()); - this.proxyHandler.enableProxyUrl(EXISTING_UUID_IS_PROXY_BINARY); + Assertions.assertEquals("http://localhost:8080", url); - Assertions.assertFalse(this.proxyHandler.getConfig().containsKey(EXISTING_UUID_IS_PROXY_BINARY.toString())); - } + this.proxyHandler.enableProxyUrl(EXISTING_UUID_IS_PROXY_BINARY); - private StubMapping createDefaultStubMapping(final UUID id, final boolean isProxy) { - final StubMapping stubMapping = new StubMapping(); - final String proxyUrl = isProxy ? "http://localhost:8080" : null; - final ResponseDefinition responseDefinition = new ResponseDefinition(200, "test", "test", null, null, null, null, null, null, null, - null, - proxyUrl, null, null, null, null, null); + Assertions.assertFalse( + this.proxyHandler.getConfig().containsKey(EXISTING_UUID_IS_PROXY_BINARY.toString())); + } - stubMapping.setResponse(responseDefinition); - stubMapping.setId(id); + private StubMapping createDefaultStubMapping(final UUID id, final boolean isProxy) { + final StubMapping stubMapping = new StubMapping(); + final String proxyUrl = isProxy ? "http://localhost:8080" : null; + final ResponseDefinition responseDefinition = + new ResponseDefinition( + 200, "test", "test", null, null, null, null, null, null, null, null, null, proxyUrl, + null, null, null, null, null); - return stubMapping; - } + stubMapping.setResponse(responseDefinition); + stubMapping.setId(id); - private StubMapping createBinaryStubMapping() { - final StubMapping stubMapping = new StubMapping(); - final ResponseDefinition responseDefinition = new ResponseDefinition(200, "test", new byte[0], null, null, null, null, null, null, - null, - null, - "http://localhost:8080", null, null, null, null, null); + return stubMapping; + } - stubMapping.setResponse(responseDefinition); - stubMapping.setId(EXISTING_UUID_IS_PROXY_BINARY); + private StubMapping createBinaryStubMapping() { + final StubMapping stubMapping = new StubMapping(); + final ResponseDefinition responseDefinition = + new ResponseDefinition( + 200, + "test", + new byte[0], + null, + null, + null, + null, + null, + null, + null, + null, + null, + "http://localhost:8080", + null, + null, + null, + null, + null); - return stubMapping; - } + stubMapping.setResponse(responseDefinition); + stubMapping.setId(EXISTING_UUID_IS_PROXY_BINARY); -} \ No newline at end of file + return stubMapping; + } +} diff --git a/src/test/java/com/github/tomakehurst/wiremock/extension/responsetemplating/helpers/HandlebarsHelperTestBase.java b/src/test/java/com/github/tomakehurst/wiremock/extension/responsetemplating/helpers/HandlebarsHelperTestBase.java index 5378bceca8..556611f46b 100644 --- a/src/test/java/com/github/tomakehurst/wiremock/extension/responsetemplating/helpers/HandlebarsHelperTestBase.java +++ b/src/test/java/com/github/tomakehurst/wiremock/extension/responsetemplating/helpers/HandlebarsHelperTestBase.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2017-2023 Thomas Akehurst + * Copyright (C) 2017-2024 Thomas Akehurst * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -24,8 +24,8 @@ import com.github.jknack.handlebars.Helper; import com.github.jknack.handlebars.Options; import com.github.tomakehurst.wiremock.client.ResponseDefinitionBuilder; +import com.github.tomakehurst.wiremock.common.RequestCache; import com.github.tomakehurst.wiremock.extension.ResponseDefinitionTransformerV2; -import com.github.tomakehurst.wiremock.extension.responsetemplating.RenderCache; import com.github.tomakehurst.wiremock.extension.responsetemplating.ResponseTemplateTransformer; import com.github.tomakehurst.wiremock.http.Request; import com.github.tomakehurst.wiremock.http.ResponseDefinition; @@ -40,12 +40,12 @@ public abstract class HandlebarsHelperTestBase { protected ResponseTemplateTransformer transformer; - protected RenderCache renderCache; + protected RequestCache requestCache; @BeforeEach - public void initRenderCache() { + public void initRequestCache() { transformer = buildTemplateTransformer(true); - renderCache = new RenderCache(); + requestCache = new RequestCache(); } protected static final String FAIL_GRACEFULLY_MSG = @@ -82,23 +82,23 @@ protected Options createOptions(Object... optionParams) { } protected Options createOptions(Map hash, Object... optionParams) { - return createOptions(renderCache, hash, optionParams); + return createOptions(requestCache, hash, optionParams); } protected Options createOptions( - RenderCache renderCache, Map hash, Object... optionParams) { - Context context = createContext(renderCache); + RequestCache requestCache, Map hash, Object... optionParams) { + Context context = createContext(requestCache); return new Options( null, null, null, context, null, null, optionParams, hash, new ArrayList(0)); } protected Context createContext() { - return createContext(renderCache); + return createContext(requestCache); } - private Context createContext(RenderCache renderCache) { - return Context.newBuilder(null).combine("renderCache", renderCache).build(); + private Context createContext(RequestCache requestCache) { + return Context.newBuilder(null).combine("requestCache", requestCache).build(); } protected static Map map() { diff --git a/src/test/java/com/github/tomakehurst/wiremock/extension/responsetemplating/helpers/HandlebarsJsonPathHelperTest.java b/src/test/java/com/github/tomakehurst/wiremock/extension/responsetemplating/helpers/HandlebarsJsonPathHelperTest.java index b455506920..2711536159 100644 --- a/src/test/java/com/github/tomakehurst/wiremock/extension/responsetemplating/helpers/HandlebarsJsonPathHelperTest.java +++ b/src/test/java/com/github/tomakehurst/wiremock/extension/responsetemplating/helpers/HandlebarsJsonPathHelperTest.java @@ -340,7 +340,7 @@ public void returnsCorrectResultWhenDifferentExpressionsUsedOnSameDocument() thr } @Test - public void helperCanBeCalledDirectlyWithoutSupplyingRenderCache() throws Exception { + public void helperCanBeCalledDirectlyWithoutSupplyingRequestCache() throws Exception { Context context = Context.newBuilder(null).build(); Options options = new Options( diff --git a/src/test/java/com/github/tomakehurst/wiremock/extension/responsetemplating/helpers/SystemValueHelperTest.java b/src/test/java/com/github/tomakehurst/wiremock/extension/responsetemplating/helpers/SystemValueHelperTest.java index b1b200c3ab..ffa3a2ade7 100644 --- a/src/test/java/com/github/tomakehurst/wiremock/extension/responsetemplating/helpers/SystemValueHelperTest.java +++ b/src/test/java/com/github/tomakehurst/wiremock/extension/responsetemplating/helpers/SystemValueHelperTest.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2019-2023 Thomas Akehurst + * Copyright (C) 2019-2024 Thomas Akehurst * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -21,9 +21,10 @@ import com.github.tomakehurst.wiremock.common.ConsoleNotifier; import com.github.tomakehurst.wiremock.common.LocalNotifier; import com.github.tomakehurst.wiremock.extension.responsetemplating.SystemKeyAuthoriser; -import java.io.IOException; import java.util.Map; +import java.util.Optional; import java.util.Set; +import org.junit.jupiter.api.Assumptions; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junitpioneer.jupiter.ClearSystemProperty; @@ -39,16 +40,28 @@ public void init() { } @Test - public void getExistingEnvironmentVariableShouldNotNull() throws Exception { - Map optionsHash = Map.of("key", "PATH", "type", "ENVIRONMENT"); + public void getExistingEnvironmentVariableShouldNotNull() { + Optional key = System.getenv().keySet().stream().findFirst(); + Assumptions.assumeTrue(key.isPresent()); + Map optionsHash = Map.of("key", key.get(), "type", "ENVIRONMENT"); String output = render(optionsHash); - assertNotNull(output); - assertTrue(output.length() > 0); + assertEquals(System.getenv(key.get()), output); } @Test - public void getNonExistingEnvironmentVariableShouldNull() throws Exception { + public void getExistingEnvironmentVariableWithDefault() { + Optional key = System.getenv().keySet().stream().findFirst(); + Assumptions.assumeTrue(key.isPresent()); + Map optionsHash = + Map.of("key", key.get(), "type", "ENVIRONMENT", "default", "DEFAULT"); + + String output = render(optionsHash); + assertEquals(System.getenv(key.get()), output); + } + + @Test + public void getNonExistingEnvironmentVariableShouldNull() { Map optionsHash = Map.of("key", "NON_EXISTING_VAR", "type", "ENVIRONMENT"); String output = render(optionsHash); @@ -56,7 +69,16 @@ public void getNonExistingEnvironmentVariableShouldNull() throws Exception { } @Test - public void getForbiddenEnvironmentVariableShouldReturnError() throws Exception { + public void getNonExistingEnvironmentVariableWithDefault() { + Map optionsHash = + Map.of("key", "NON_EXISTING_VAR", "type", "ENVIRONMENT", "default", "DEFAULT"); + + String output = render(optionsHash); + assertEquals("DEFAULT", output); + } + + @Test + public void getForbiddenEnvironmentVariableShouldReturnError() { helper = new SystemValueHelper(new SystemKeyAuthoriser(Set.of("JAVA*"))); Map optionsHash = Map.of("key", "TEST_VAR", "type", "ENVIRONMENT"); @@ -65,7 +87,7 @@ public void getForbiddenEnvironmentVariableShouldReturnError() throws Exception } @Test - public void getEmptyKeyShouldReturnError() throws Exception { + public void getEmptyKeyShouldReturnError() { Map optionsHash = Map.of("key", "", "type", "PROPERTY"); String value = render(optionsHash); assertEquals("[ERROR: The key cannot be empty]", value); @@ -73,7 +95,7 @@ public void getEmptyKeyShouldReturnError() throws Exception { @Test @ClearSystemProperty(key = "test.key") - public void getAllowedPropertyShouldSuccess() throws Exception { + public void getAllowedPropertyShouldSuccess() { helper = new SystemValueHelper(new SystemKeyAuthoriser(Set.of("test.*"))); System.setProperty("test.key", "aaa"); assertEquals("aaa", System.getProperty("test.key")); @@ -84,7 +106,19 @@ public void getAllowedPropertyShouldSuccess() throws Exception { @Test @ClearSystemProperty(key = "test.key") - public void getForbiddenPropertyShouldReturnError() throws Exception { + public void getAllowedPropertyWithDefault() { + helper = new SystemValueHelper(new SystemKeyAuthoriser(Set.of("test.*"))); + System.setProperty("test.key", "aaa"); + assertEquals("aaa", System.getProperty("test.key")); + Map optionsHash = + Map.of("key", "test.key", "type", "PROPERTY", "default", "DEFAULT"); + String value = render(optionsHash); + assertEquals("aaa", value); + } + + @Test + @ClearSystemProperty(key = "test.key") + public void getForbiddenPropertyShouldReturnError() { helper = new SystemValueHelper(new SystemKeyAuthoriser(Set.of("JAVA.*"))); System.setProperty("test.key", "aaa"); Map optionsHash = Map.of("key", "test.key", "type", "PROPERTY"); @@ -93,13 +127,31 @@ public void getForbiddenPropertyShouldReturnError() throws Exception { } @Test - public void getNonExistingSystemPropertyShouldNull() throws Exception { + public void getNonExistingSystemPropertyShouldNull() { Map optionsHash = Map.of("key", "not.existing.prop", "type", "PROPERTY"); String output = render(optionsHash); assertNull(output); } - private String render(Map optionsHash) throws IOException { + @Test + public void getNonExistingSystemPropertyWithDefault() { + Map optionsHash = + Map.of("key", "not.existing.prop", "type", "PROPERTY", "default", "DEFAULT"); + String output = render(optionsHash); + assertEquals("DEFAULT", output); + } + + @Test + public void getDefaultType() { + Optional key = System.getenv().keySet().stream().findFirst(); + Assumptions.assumeTrue(key.isPresent()); + Map optionsHash = Map.of("key", key.get()); + + String output = render(optionsHash); + assertEquals(System.getenv(key.get()), output); + } + + private String render(Map optionsHash) { return helper.apply( null, new Options.Builder(null, null, null, null, null).setHash(optionsHash).build()); } diff --git a/src/test/java/com/github/tomakehurst/wiremock/extension/webhooks/WebhooksRegistrationTest.java b/src/test/java/com/github/tomakehurst/wiremock/extension/webhooks/WebhooksRegistrationTest.java index 4b3e16d0f2..01a2f46566 100644 --- a/src/test/java/com/github/tomakehurst/wiremock/extension/webhooks/WebhooksRegistrationTest.java +++ b/src/test/java/com/github/tomakehurst/wiremock/extension/webhooks/WebhooksRegistrationTest.java @@ -33,9 +33,10 @@ class WebhooksRegistrationTest { private static final String MESSAGE = "Passing webhooks in extensions is no longer required and" + " may lead to compatibility issues in future"; - private final PrintStream stdOut = System.out; private WireMockServerRunner runner; private WireMockServer server; + + private final PrintStream stdOut = System.out; private ByteArrayOutputStream out; @BeforeEach @@ -72,14 +73,16 @@ private String getSystemOutText() { @Test void shouldLogMessageWhenWebhooksAreAddedViaClassName() { - server = new WireMockServer(wireMockConfig().extensions("org.wiremock.webhooks.Webhooks")); + server = + new WireMockServer( + wireMockConfig().extensions("org.wiremock.webhooks.Webhooks").dynamicPort()); server.start(); assertThat(getSystemOutText(), containsString(MESSAGE)); } @Test void shouldLogMessageWhenWebhooksAreAddedViaClass() { - server = new WireMockServer(wireMockConfig().extensions(Webhooks.class)); + server = new WireMockServer(wireMockConfig().extensions(Webhooks.class).dynamicPort()); server.start(); assertThat(getSystemOutText(), containsString(MESSAGE)); } @@ -87,14 +90,14 @@ void shouldLogMessageWhenWebhooksAreAddedViaClass() { @Test void shouldLogAMessageWhenWebhooksAreAddedViaCLI() { runner = new WireMockServerRunner(); - runner.run("--extensions", "org.wiremock.webhooks.Webhooks"); + runner.run("--extensions", "org.wiremock.webhooks.Webhooks", "--port", "0"); assertThat(getSystemOutText(), containsString(MESSAGE)); stopRunner(); } @Test void shouldNotLogAMessageWhenWebhooksAreNotAddedExplicitly() { - server = new WireMockServer(wireMockConfig()); + server = new WireMockServer(wireMockConfig().dynamicPort()); server.start(); assertThat(getSystemOutText(), not(containsString(MESSAGE))); } diff --git a/src/test/java/com/github/tomakehurst/wiremock/http/ContentTypeHeaderTest.java b/src/test/java/com/github/tomakehurst/wiremock/http/ContentTypeHeaderTest.java index a5a6e4c092..53bb66b3e2 100644 --- a/src/test/java/com/github/tomakehurst/wiremock/http/ContentTypeHeaderTest.java +++ b/src/test/java/com/github/tomakehurst/wiremock/http/ContentTypeHeaderTest.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2011-2023 Thomas Akehurst + * Copyright (C) 2011-2024 Thomas Akehurst * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -15,12 +15,12 @@ */ package com.github.tomakehurst.wiremock.http; +import static java.nio.charset.StandardCharsets.UTF_8; import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.Matchers.is; import static org.hamcrest.Matchers.nullValue; import static org.junit.jupiter.api.Assertions.*; -import com.github.tomakehurst.wiremock.common.Strings; import com.github.tomakehurst.wiremock.testsupport.MockRequestBuilder; import java.nio.charset.StandardCharsets; import java.util.Optional; @@ -94,12 +94,12 @@ public void returnsCharsetWhenPresent() { @Test public void returnsDefaultCharsetWhenEncodingNotPresent() { ContentTypeHeader header = new ContentTypeHeader("text/plain"); - assertThat(header.charset(), is(Strings.DEFAULT_CHARSET)); + assertThat(header.charset(), is(UTF_8)); } @Test public void returnsDefaultCharsetWhenAbsent() { ContentTypeHeader header = ContentTypeHeader.absent(); - assertThat(header.charset(), is(Strings.DEFAULT_CHARSET)); + assertThat(header.charset(), is(UTF_8)); } } diff --git a/src/test/java/com/github/tomakehurst/wiremock/http/ProxyResponseRendererTest.java b/src/test/java/com/github/tomakehurst/wiremock/http/ProxyResponseRendererTest.java index b60c00e818..a67d26d774 100644 --- a/src/test/java/com/github/tomakehurst/wiremock/http/ProxyResponseRendererTest.java +++ b/src/test/java/com/github/tomakehurst/wiremock/http/ProxyResponseRendererTest.java @@ -18,6 +18,7 @@ import static com.github.tomakehurst.wiremock.client.WireMock.*; import static com.github.tomakehurst.wiremock.core.WireMockConfiguration.options; import static com.github.tomakehurst.wiremock.crypto.X509CertificateVersion.V3; +import static com.github.tomakehurst.wiremock.http.RequestMethod.GET; import static com.github.tomakehurst.wiremock.matching.MockRequest.mockRequest; import static com.github.tomakehurst.wiremock.stubbing.ServeEventFactory.newPostMatchServeEvent; import static java.net.HttpURLConnection.HTTP_INTERNAL_ERROR; @@ -26,7 +27,9 @@ import static org.hamcrest.core.StringContains.containsString; import static org.hamcrest.core.StringStartsWith.startsWith; import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; import static org.mockito.ArgumentMatchers.argThat; +import static org.mockito.ArgumentMatchers.assertArg; import static org.mockito.Mockito.spy; import com.github.tomakehurst.wiremock.common.NetworkAddressRules; @@ -49,11 +52,11 @@ import java.security.KeyPair; import java.security.KeyPairGenerator; import java.security.NoSuchAlgorithmException; -import java.util.Collections; -import java.util.Date; -import java.util.List; +import java.util.*; +import java.util.stream.Collectors; import org.apache.hc.client5.http.config.RequestConfig; import org.apache.hc.client5.http.impl.classic.CloseableHttpClient; +import org.apache.hc.core5.http.NameValuePair; import org.apache.hc.core5.http.io.HttpClientResponseHandler; import org.assertj.core.api.Assertions; import org.junit.jupiter.api.Test; @@ -131,7 +134,7 @@ public void acceptsSelfSignedCertificateForForwardProxyingIfTrustAllProxyTargets @Test void passesThroughCorsResponseHeadersWhenStubCorsDisabled() { - ProxyResponseRenderer responseRenderer = buildProxyResponseRenderer(true, false); + ProxyResponseRenderer responseRenderer = buildProxyResponseRenderer(true, false, null); origin.stubFor( get("/proxied") @@ -150,7 +153,7 @@ void passesThroughCorsResponseHeadersWhenStubCorsDisabled() { @Test void doesNotPassThroughCorsResponseHeadersWhenStubCorsEnabled() { - ProxyResponseRenderer responseRenderer = buildProxyResponseRenderer(true, true); + ProxyResponseRenderer responseRenderer = buildProxyResponseRenderer(true, true, null); origin.stubFor( get("/proxied") @@ -248,7 +251,7 @@ void addsEmptyEntityIfEmptyBodyForwardProxyGET() throws IOException { "/proxied/empty-get", true, new byte[0], - RequestMethod.GET, + GET, new HttpHeaders(new HttpHeader("Content-Length", "0"))); trustAllProxyResponseRenderer.render(serveEvent); @@ -279,6 +282,120 @@ void usesCorrectProxyRequestTimeout() { is((long) PROXY_TIMEOUT)); } + @Test + void additionalProxyRequestHeaders() throws IOException { + ServeEvent serveEvent = + serveEvent( + "/proxied", + false, + null, + RequestMethod.GET, + new HttpHeaders(), + aResponse() + .proxiedFrom(origin.baseUrl()) + .withAdditionalRequestHeader("header", "value") + .build()); + + proxyResponseRenderer.render(serveEvent); + Mockito.verify(reverseProxyApacheClient) + .execute( + argThat(request -> request.getFirstHeader("header").getValue().equals("value")), + ArgumentMatchers.any(HttpClientResponseHandler.class)); + } + + @Test + void removeProxyRequestHeaders() throws IOException { + ServeEvent serveEvent = + serveEvent( + "/proxied", + false, + null, + RequestMethod.GET, + new HttpHeaders(new HttpHeader("header", "value")), + aResponse().proxiedFrom(origin.baseUrl()).withRemoveRequestHeader("Header").build()); + + proxyResponseRenderer.render(serveEvent); + Mockito.verify(reverseProxyApacheClient) + .execute( + argThat(request -> request.getHeaders().length == 0), + ArgumentMatchers.any(HttpClientResponseHandler.class)); + } + + @Test + void maintainsAcceptEncodingIfNoSupportedEncodingsSpecified() throws IOException { + + Set supportedProxyEncodings = null; + ProxyResponseRenderer proxyResponseRenderer = + buildProxyResponseRenderer(false, false, supportedProxyEncodings); + ServeEvent serveEvent = + serveEvent( + "/proxied", + false, + new byte[0], + GET, + new HttpHeaders(HttpHeader.httpHeader("Accept-Encoding", "gzip,br"))); + + proxyResponseRenderer.render(serveEvent); + Mockito.verify(reverseProxyApacheClient) + .execute( + assertArg( + request -> + assertThat( + Arrays.stream(request.getHeaders("Accept-Encoding")) + .map(NameValuePair::getValue) + .collect(Collectors.toList()), + is(List.of("gzip,br")))), + ArgumentMatchers.any(HttpClientResponseHandler.class)); + } + + @Test + void limitsAcceptEncodingToSupportedEncodings() throws IOException { + + Set supportedProxyEncodings = Set.of("gzip", "br"); + ProxyResponseRenderer proxyResponseRenderer = + buildProxyResponseRenderer(false, false, supportedProxyEncodings); + ServeEvent serveEvent = + serveEvent( + "/proxied", + false, + new byte[0], + GET, + new HttpHeaders(HttpHeader.httpHeader("Accept-Encoding", "gzip,deflate,br"))); + + proxyResponseRenderer.render(serveEvent); + Mockito.verify(reverseProxyApacheClient) + .execute( + assertArg( + request -> + assertThat( + Arrays.stream(request.getHeaders("Accept-Encoding")) + .map(NameValuePair::getValue) + .collect(Collectors.toList()), + is(List.of("gzip,br")))), + ArgumentMatchers.any(HttpClientResponseHandler.class)); + } + + @Test + void removesAcceptEncodingIfNoneSupported() throws IOException { + + Set supportedProxyEncodings = Set.of("gzip"); + ProxyResponseRenderer proxyResponseRenderer = + buildProxyResponseRenderer(false, false, supportedProxyEncodings); + ServeEvent serveEvent = + serveEvent( + "/proxied", + false, + new byte[0], + GET, + new HttpHeaders(HttpHeader.httpHeader("Accept-Encoding", "deflate,br"))); + + proxyResponseRenderer.render(serveEvent); + Mockito.verify(reverseProxyApacheClient) + .execute( + assertArg(request -> assertFalse(request.containsHeader("Accept-Encoding"))), + ArgumentMatchers.any(HttpClientResponseHandler.class)); + } + private static T reflectiveInnerSpyField( Class fieldType, String outerFieldName, String innerFieldName, Object object) { try { @@ -295,6 +412,27 @@ private static T reflectiveInnerSpyField( } } + @Test + void proxyUrlPrefixToRemove() throws IOException { + ServeEvent serveEvent = + serveEvent( + "/prefix/proxied", + false, + null, + RequestMethod.GET, + new HttpHeaders(new HttpHeader("header", "value")), + aResponse() + .proxiedFrom(origin.baseUrl()) + .withProxyUrlPrefixToRemove("/prefix") + .build()); + + proxyResponseRenderer.render(serveEvent); + Mockito.verify(reverseProxyApacheClient) + .execute( + argThat(request -> request.getRequestUri().equals("/proxied")), + ArgumentMatchers.any(HttpClientResponseHandler.class)); + } + private static T reflectiveSpyField(Class fieldType, String fieldName, Object object) { try { Field field = object.getClass().getDeclaredField(fieldName); @@ -307,10 +445,6 @@ private static T reflectiveSpyField(Class fieldType, String fieldName, Ob } } - private static T spyField(T object) { - return spy(object); - } - private ServeEvent reverseProxyServeEvent(String path) { return serveEvent(path, false, new byte[0]); } @@ -320,7 +454,7 @@ private ServeEvent forwardProxyServeEvent(String path) { } private ServeEvent serveEvent(String path, boolean isBrowserProxyRequest, byte[] body) { - return serveEvent(path, isBrowserProxyRequest, body, RequestMethod.GET, new HttpHeaders()); + return serveEvent(path, isBrowserProxyRequest, body, GET, new HttpHeaders()); } private ServeEvent serveEvent( @@ -329,6 +463,22 @@ private ServeEvent serveEvent( byte[] body, RequestMethod method, HttpHeaders headers) { + return serveEvent( + path, + isBrowserProxyRequest, + body, + method, + headers, + aResponse().proxiedFrom(origin.baseUrl()).build()); + } + + private ServeEvent serveEvent( + String path, + boolean isBrowserProxyRequest, + byte[] body, + RequestMethod method, + HttpHeaders headers, + ResponseDefinition responseDefinition) { LoggedRequest loggedRequest = LoggedRequest.createFrom( @@ -340,7 +490,6 @@ private ServeEvent serveEvent( .isBrowserProxyRequest(isBrowserProxyRequest) .body(body) .protocol("HTTP/1.1")); - ResponseDefinition responseDefinition = aResponse().proxiedFrom(origin.baseUrl()).build(); responseDefinition.setOriginalRequest(loggedRequest); return newPostMatchServeEvent(loggedRequest, responseDefinition); @@ -375,11 +524,11 @@ private KeyPair generateKeyPair() throws NoSuchAlgorithmException { } private ProxyResponseRenderer buildProxyResponseRenderer(boolean trustAllProxyTargets) { - return buildProxyResponseRenderer(trustAllProxyTargets, false); + return buildProxyResponseRenderer(trustAllProxyTargets, false, null); } private ProxyResponseRenderer buildProxyResponseRenderer( - boolean trustAllProxyTargets, boolean stubCorsEnabled) { + boolean trustAllProxyTargets, boolean stubCorsEnabled, Set supportedProxyEncodings) { reverseProxyApacheClient = spy( @@ -414,6 +563,7 @@ private ProxyResponseRenderer buildProxyResponseRenderer( /* hostHeaderValue= */ null, new InMemorySettingsStore(), stubCorsEnabled, + supportedProxyEncodings, reverseProxyClient, forwardProxyClient); } diff --git a/src/test/java/com/github/tomakehurst/wiremock/http/StubResponseRendererTest.java b/src/test/java/com/github/tomakehurst/wiremock/http/StubResponseRendererTest.java index 04aee99210..4053ea5578 100644 --- a/src/test/java/com/github/tomakehurst/wiremock/http/StubResponseRendererTest.java +++ b/src/test/java/com/github/tomakehurst/wiremock/http/StubResponseRendererTest.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2017-2023 Thomas Akehurst + * Copyright (C) 2017-2024 Thomas Akehurst * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -121,6 +121,7 @@ private ServeEvent createServeEvent(Integer fixedDelayMillis) { "", null, null, + null, fixedDelayMillis, null, null, diff --git a/src/test/java/com/github/tomakehurst/wiremock/Http2AcceptanceTest.java b/src/test/java/com/github/tomakehurst/wiremock/jetty11/Http2AcceptanceTest.java similarity index 92% rename from src/test/java/com/github/tomakehurst/wiremock/Http2AcceptanceTest.java rename to src/test/java/com/github/tomakehurst/wiremock/jetty11/Http2AcceptanceTest.java index 830e092fc0..eb222263c7 100644 --- a/src/test/java/com/github/tomakehurst/wiremock/Http2AcceptanceTest.java +++ b/src/test/java/com/github/tomakehurst/wiremock/jetty11/Http2AcceptanceTest.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2019-2021 Thomas Akehurst + * Copyright (C) 2019-2024 Thomas Akehurst * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package com.github.tomakehurst.wiremock; +package com.github.tomakehurst.wiremock.jetty11; import static com.github.tomakehurst.wiremock.client.WireMock.get; import static com.github.tomakehurst.wiremock.client.WireMock.ok; @@ -47,6 +47,7 @@ public void supportsHttp2Connections() throws Exception { wm.stubFor(get("/thing").willReturn(ok("HTTP/2 response"))); ContentResponse response = client.GET(wm.getRuntimeInfo().getHttpsBaseUrl() + "/thing"); + assertThat(response.getVersion(), is(HTTP_2)); assertThat(response.getStatus(), is(200)); } @@ -56,7 +57,7 @@ public void supportsHttp2PlaintextConnections() throws Exception { wm.stubFor(get("/thing").willReturn(ok("HTTP/2 response"))); - ContentResponse response = client.GET(wm.url("/thing")); + ContentResponse response = client.GET(wm.getRuntimeInfo().getHttpBaseUrl() + "/thing"); assertThat(response.getVersion(), is(HTTP_2)); assertThat(response.getStatus(), is(200)); } diff --git a/src/test/java/com/github/tomakehurst/wiremock/Http2ClientFactory.java b/src/test/java/com/github/tomakehurst/wiremock/jetty11/Http2ClientFactory.java similarity index 92% rename from src/test/java/com/github/tomakehurst/wiremock/Http2ClientFactory.java rename to src/test/java/com/github/tomakehurst/wiremock/jetty11/Http2ClientFactory.java index baf270bf4e..56e6528c5a 100644 --- a/src/test/java/com/github/tomakehurst/wiremock/Http2ClientFactory.java +++ b/src/test/java/com/github/tomakehurst/wiremock/jetty11/Http2ClientFactory.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2019-2022 Thomas Akehurst + * Copyright (C) 2019-2024 Thomas Akehurst * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package com.github.tomakehurst.wiremock; +package com.github.tomakehurst.wiremock.jetty11; import com.github.tomakehurst.wiremock.common.Exceptions; import org.eclipse.jetty.client.HttpClient; @@ -23,8 +23,7 @@ import org.eclipse.jetty.io.ClientConnector; import org.eclipse.jetty.util.ssl.SslContextFactory; -public class Http2ClientFactory { - +class Http2ClientFactory { public static HttpClient create() { final SslContextFactory.Client sslContextFactory = new SslContextFactory.Client(true); final ClientConnector connector = new ClientConnector(); diff --git a/src/test/java/com/github/tomakehurst/wiremock/jetty11/Http2DisabledAcceptanceTest.java b/src/test/java/com/github/tomakehurst/wiremock/jetty11/Http2DisabledAcceptanceTest.java new file mode 100644 index 0000000000..e83861b71f --- /dev/null +++ b/src/test/java/com/github/tomakehurst/wiremock/jetty11/Http2DisabledAcceptanceTest.java @@ -0,0 +1,118 @@ +/* + * Copyright (C) 2024 Thomas Akehurst + * + * 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 + * + * http://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. + */ +package com.github.tomakehurst.wiremock.jetty11; + +import static com.github.tomakehurst.wiremock.client.WireMock.get; +import static com.github.tomakehurst.wiremock.client.WireMock.ok; +import static com.github.tomakehurst.wiremock.core.WireMockConfiguration.wireMockConfig; +import static java.net.http.HttpClient.Version.HTTP_1_1; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.is; + +import com.github.tomakehurst.wiremock.junit5.WireMockExtension; +import java.net.Socket; +import java.net.URI; +import java.net.http.HttpClient; +import java.net.http.HttpRequest; +import java.net.http.HttpResponse; +import java.security.SecureRandom; +import java.security.cert.X509Certificate; +import javax.net.ssl.SSLContext; +import javax.net.ssl.SSLEngine; +import javax.net.ssl.TrustManager; +import javax.net.ssl.X509ExtendedTrustManager; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +public class Http2DisabledAcceptanceTest { + + @RegisterExtension + public WireMockExtension wm = + WireMockExtension.newInstance() + .options( + wireMockConfig() + .dynamicPort() + .dynamicHttpsPort() + .http2PlainDisabled(true) + .http2TlsDisabled(true)) + .build(); + + HttpClient client; + + @BeforeEach + void init() throws Exception { + client = HttpClient.newBuilder().sslContext(trustEverything()).build(); + } + + @Test + public void usesHttp1_1OverPlainText() throws Exception { + wm.stubFor(get("/thing").willReturn(ok("HTTP/2 response"))); + + URI uri = URI.create(wm.getRuntimeInfo().getHttpBaseUrl() + "/thing"); + + HttpResponse response = + client.send(HttpRequest.newBuilder(uri).build(), HttpResponse.BodyHandlers.ofString()); + assertThat(response.version(), is(HTTP_1_1)); + assertThat(response.statusCode(), is(200)); + } + + @Test + void usesHttp1_1OverTls() throws Exception { + wm.stubFor(get("/thing").willReturn(ok("HTTP/2 response"))); + + URI uri = URI.create(wm.getRuntimeInfo().getHttpsBaseUrl() + "/thing"); + + HttpResponse response = + client.send(HttpRequest.newBuilder(uri).build(), HttpResponse.BodyHandlers.ofString()); + assertThat(response.version(), is(HTTP_1_1)); + assertThat(response.statusCode(), is(200)); + } + + private SSLContext trustEverything() throws Exception { + X509ExtendedTrustManager trustManager = + new X509ExtendedTrustManager() { + @Override + public X509Certificate[] getAcceptedIssuers() { + return new X509Certificate[] {}; + } + + @Override + public void checkClientTrusted(X509Certificate[] chain, String authType) {} + + @Override + public void checkServerTrusted(X509Certificate[] chain, String authType) {} + + @Override + public void checkClientTrusted(X509Certificate[] chain, String authType, Socket socket) {} + + @Override + public void checkServerTrusted(X509Certificate[] chain, String authType, Socket socket) {} + + @Override + public void checkClientTrusted( + X509Certificate[] chain, String authType, SSLEngine engine) {} + + @Override + public void checkServerTrusted( + X509Certificate[] chain, String authType, SSLEngine engine) {} + }; + SSLContext sslContext = SSLContext.getInstance("TLS"); + sslContext.init(null, new TrustManager[] {trustManager}, new SecureRandom()); + + return sslContext; + } +} diff --git a/src/test/java/com/github/tomakehurst/wiremock/jetty11/MultipartParser.java b/src/test/java/com/github/tomakehurst/wiremock/jetty11/MultipartParser.java index 1092981252..f01d1f1255 100644 --- a/src/test/java/com/github/tomakehurst/wiremock/jetty11/MultipartParser.java +++ b/src/test/java/com/github/tomakehurst/wiremock/jetty11/MultipartParser.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2018-2023 Thomas Akehurst + * Copyright (C) 2018-2024 Thomas Akehurst * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -24,10 +24,10 @@ import java.util.stream.Collectors; import org.eclipse.jetty.server.MultiPartInputStreamParser; -public class MultipartParser { - - @SuppressWarnings("unchecked") - public static Collection parse(byte[] body, String contentType) { +public class MultipartParser + implements com.github.tomakehurst.wiremock.MultipartParserLoader.MultipartParser { + @Override + public Collection parse(byte[] body, String contentType) { MultiPartInputStreamParser parser = new MultiPartInputStreamParser(new ByteArrayInputStream(body), contentType, null, null); try { diff --git a/src/test/java/com/github/tomakehurst/wiremock/jetty11/MultipartParserLoader.java b/src/test/java/com/github/tomakehurst/wiremock/jetty11/MultipartParserLoader.java new file mode 100644 index 0000000000..52883f5a13 --- /dev/null +++ b/src/test/java/com/github/tomakehurst/wiremock/jetty11/MultipartParserLoader.java @@ -0,0 +1,32 @@ +/* + * Copyright (C) 2018-2024 Thomas Akehurst + * + * 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 + * + * http://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. + */ +package com.github.tomakehurst.wiremock.jetty11; + +import java.util.Optional; + +public class MultipartParserLoader + implements com.github.tomakehurst.wiremock.MultipartParserLoader { + private static final String JETTY_11 = "11"; /* Jetty 11 */ + + @Override + public Optional getMultipartParser(String jettyMajorVersion) { + if (JETTY_11.equalsIgnoreCase(jettyMajorVersion)) { + return Optional.of(new com.github.tomakehurst.wiremock.jetty11.MultipartParser()); + } else { + return Optional.empty(); + } + } +} diff --git a/src/test/java/com/github/tomakehurst/wiremock/WarDeploymentAcceptanceTest.java b/src/test/java/com/github/tomakehurst/wiremock/jetty11/WarDeploymentAcceptanceTest.java similarity index 97% rename from src/test/java/com/github/tomakehurst/wiremock/WarDeploymentAcceptanceTest.java rename to src/test/java/com/github/tomakehurst/wiremock/jetty11/WarDeploymentAcceptanceTest.java index b266892258..b7ae98a3b7 100644 --- a/src/test/java/com/github/tomakehurst/wiremock/WarDeploymentAcceptanceTest.java +++ b/src/test/java/com/github/tomakehurst/wiremock/jetty11/WarDeploymentAcceptanceTest.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2012-2021 Thomas Akehurst + * Copyright (C) 2012-2024 Thomas Akehurst * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package com.github.tomakehurst.wiremock; +package com.github.tomakehurst.wiremock.jetty11; import static com.github.tomakehurst.wiremock.client.WireMock.*; import static com.github.tomakehurst.wiremock.testsupport.TestFiles.sampleWarRootDir; diff --git a/src/test/java/com/github/tomakehurst/wiremock/WarDeploymentParameterAcceptanceTest.java b/src/test/java/com/github/tomakehurst/wiremock/jetty11/WarDeploymentParameterAcceptanceTest.java similarity index 97% rename from src/test/java/com/github/tomakehurst/wiremock/WarDeploymentParameterAcceptanceTest.java rename to src/test/java/com/github/tomakehurst/wiremock/jetty11/WarDeploymentParameterAcceptanceTest.java index 1a2cd9279d..618345b7dd 100644 --- a/src/test/java/com/github/tomakehurst/wiremock/WarDeploymentParameterAcceptanceTest.java +++ b/src/test/java/com/github/tomakehurst/wiremock/jetty11/WarDeploymentParameterAcceptanceTest.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2014-2021 Thomas Akehurst + * Copyright (C) 2014-2024 Thomas Akehurst * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package com.github.tomakehurst.wiremock; +package com.github.tomakehurst.wiremock.jetty11; import static com.github.tomakehurst.wiremock.client.WireMock.*; import static com.github.tomakehurst.wiremock.testsupport.TestFiles.sampleWarRootDir; diff --git a/src/test/java/com/github/tomakehurst/wiremock/archunit/UnusedCodeTest.java b/src/test/java/com/github/tomakehurst/wiremock/jetty11/archunit/UnusedCodeTest.java similarity index 98% rename from src/test/java/com/github/tomakehurst/wiremock/archunit/UnusedCodeTest.java rename to src/test/java/com/github/tomakehurst/wiremock/jetty11/archunit/UnusedCodeTest.java index f27a9e1626..9caf248b24 100644 --- a/src/test/java/com/github/tomakehurst/wiremock/archunit/UnusedCodeTest.java +++ b/src/test/java/com/github/tomakehurst/wiremock/jetty11/archunit/UnusedCodeTest.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2021-2023 Thomas Akehurst + * Copyright (C) 2021-2024 Thomas Akehurst * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package com.github.tomakehurst.wiremock.archunit; +package com.github.tomakehurst.wiremock.jetty11.archunit; import static com.tngtech.archunit.base.DescribedPredicate.describe; import static com.tngtech.archunit.base.DescribedPredicate.not; diff --git a/src/test/java/com/github/tomakehurst/wiremock/servlet/AltHttpServerFactory.java b/src/test/java/com/github/tomakehurst/wiremock/jetty11/servlet/AltHttpServerFactory.java similarity index 95% rename from src/test/java/com/github/tomakehurst/wiremock/servlet/AltHttpServerFactory.java rename to src/test/java/com/github/tomakehurst/wiremock/jetty11/servlet/AltHttpServerFactory.java index 2e1d1ec39b..cef204ca01 100644 --- a/src/test/java/com/github/tomakehurst/wiremock/servlet/AltHttpServerFactory.java +++ b/src/test/java/com/github/tomakehurst/wiremock/jetty11/servlet/AltHttpServerFactory.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2016-2022 Thomas Akehurst + * Copyright (C) 2016-2024 Thomas Akehurst * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package com.github.tomakehurst.wiremock.servlet; +package com.github.tomakehurst.wiremock.jetty11.servlet; import static com.github.tomakehurst.wiremock.common.Exceptions.throwUnchecked; import static com.github.tomakehurst.wiremock.core.WireMockApp.ADMIN_CONTEXT_ROOT; @@ -23,6 +23,7 @@ import com.github.tomakehurst.wiremock.common.Notifier; import com.github.tomakehurst.wiremock.core.Options; import com.github.tomakehurst.wiremock.http.*; +import com.github.tomakehurst.wiremock.servlet.WireMockHandlerDispatchingServlet; import org.eclipse.jetty.server.Handler; import org.eclipse.jetty.server.Server; import org.eclipse.jetty.server.ServerConnector; diff --git a/src/test/java/com/github/tomakehurst/wiremock/servlet/AlternativeServletContainerTest.java b/src/test/java/com/github/tomakehurst/wiremock/jetty11/servlet/AlternativeServletContainerTest.java similarity index 95% rename from src/test/java/com/github/tomakehurst/wiremock/servlet/AlternativeServletContainerTest.java rename to src/test/java/com/github/tomakehurst/wiremock/jetty11/servlet/AlternativeServletContainerTest.java index c4d15c51a2..48e81123bc 100644 --- a/src/test/java/com/github/tomakehurst/wiremock/servlet/AlternativeServletContainerTest.java +++ b/src/test/java/com/github/tomakehurst/wiremock/jetty11/servlet/AlternativeServletContainerTest.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2016-2022 Thomas Akehurst + * Copyright (C) 2016-2024 Thomas Akehurst * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package com.github.tomakehurst.wiremock.servlet; +package com.github.tomakehurst.wiremock.jetty11.servlet; import static com.github.tomakehurst.wiremock.client.WireMock.*; import static com.github.tomakehurst.wiremock.core.WireMockConfiguration.options; diff --git a/src/test/java/com/github/tomakehurst/wiremock/junit5/EnabledIfJettyVersion.java b/src/test/java/com/github/tomakehurst/wiremock/junit5/EnabledIfJettyVersion.java new file mode 100644 index 0000000000..4903bf1382 --- /dev/null +++ b/src/test/java/com/github/tomakehurst/wiremock/junit5/EnabledIfJettyVersion.java @@ -0,0 +1,36 @@ +/* + * Copyright (C) 2019-2024 Thomas Akehurst + * + * 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 + * + * http://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. + */ +package com.github.tomakehurst.wiremock.junit5; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; +import org.junit.jupiter.api.extension.ExtendWith; + +/** + * Meta-annotation to enable Wiremock-specific test cases only on specific version Jetty version. + */ +@Target({ElementType.TYPE, ElementType.METHOD}) +@Retention(RetentionPolicy.RUNTIME) +@ExtendWith(EnabledIfJettyVersionCondition.class) +public @interface EnabledIfJettyVersion { + /** The minimal required version of the OpenSearch this test could run on */ + int major(); + + /** The reason (or issue reference) why this test is runnable on this version of Jetty */ + String reason(); +} diff --git a/src/test/java/com/github/tomakehurst/wiremock/junit5/EnabledIfJettyVersionCondition.java b/src/test/java/com/github/tomakehurst/wiremock/junit5/EnabledIfJettyVersionCondition.java new file mode 100644 index 0000000000..cb3978f747 --- /dev/null +++ b/src/test/java/com/github/tomakehurst/wiremock/junit5/EnabledIfJettyVersionCondition.java @@ -0,0 +1,43 @@ +/* + * Copyright (C) 2019-2024 Thomas Akehurst + * + * 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 + * + * http://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. + */ +package com.github.tomakehurst.wiremock.junit5; + +import java.lang.reflect.AnnotatedElement; +import org.eclipse.jetty.util.Jetty; +import org.junit.jupiter.api.extension.ConditionEvaluationResult; +import org.junit.jupiter.api.extension.ExecutionCondition; +import org.junit.jupiter.api.extension.ExtensionContext; + +public class EnabledIfJettyVersionCondition implements ExecutionCondition { + @Override + public ConditionEvaluationResult evaluateExecutionCondition(ExtensionContext context) { + final AnnotatedElement element = context.getElement().orElseThrow(IllegalStateException::new); + + final EnabledIfJettyVersion annotation = element.getAnnotation(EnabledIfJettyVersion.class); + if (annotation == null) { + return ConditionEvaluationResult.enabled("@EnabledIfJettyVersion is not present"); + } + + final int major = annotation.major(); + if (Jetty.VERSION.startsWith(major + ".")) { + return ConditionEvaluationResult.enabled( + "The Jetty version " + Jetty.VERSION + " matches major version " + major + " vesion"); + } else { + return ConditionEvaluationResult.disabled( + "The Jetty version " + Jetty.VERSION + " does not match major " + major + " vesion"); + } + } +} diff --git a/src/test/java/com/github/tomakehurst/wiremock/matching/MatchesJsonPathPatternTest.java b/src/test/java/com/github/tomakehurst/wiremock/matching/MatchesJsonPathPatternTest.java index f448dd58d9..7789c54c2f 100644 --- a/src/test/java/com/github/tomakehurst/wiremock/matching/MatchesJsonPathPatternTest.java +++ b/src/test/java/com/github/tomakehurst/wiremock/matching/MatchesJsonPathPatternTest.java @@ -32,11 +32,17 @@ import com.github.tomakehurst.wiremock.testsupport.ServeEventChecks; import org.hamcrest.Matchers; import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.mockito.Mockito; public class MatchesJsonPathPatternTest { + @BeforeEach + public void init() { + RequestCache.disable(); + } + @Test public void matchesABasicJsonPathWhenTheExpectedElementIsPresent() { StringValuePattern pattern = WireMock.matchingJsonPath("$.one"); @@ -55,7 +61,7 @@ public void doesNotMatchABasicJsonPathWhenTheExpectedElementIsNotPresent() { @Test public void matchesOnJsonPathsWithFilters() { - StringValuePattern pattern = WireMock.matchingJsonPath("$.numbers[?(@.number == '2')]"); + StringValuePattern pattern = WireMock.matchingJsonPath("$.numbers[?(@.number == 2)]"); assertTrue( pattern.match("{ \"numbers\": [ {\"number\": 1}, {\"number\": 2} ]}").isExactMatch(), @@ -171,12 +177,12 @@ void notifiesWhenMatchingBeingSkippedDueToContentProbablyBeingXml() { void subEventsReturnedBySubMatchersAreAddedToServeEvent() { StringValuePattern pattern = WireMock.matchingJsonPath("$.something", WireMock.equalToJson("{}")); - MatchResult matchResult = pattern.match("{ \"something\": \"{ \\\"bad:\" }"); + MatchResult matchResult = pattern.match("{ \"something\": \"{ \\\"bad\\\": }\" }"); assertFalse(matchResult.isExactMatch(), "Expected the match to fail"); ServeEventChecks.checkJsonError( matchResult, - "Unexpected end-of-input in field name\n at [Source: (String)\"{ \"bad:\"; line: 1, column: 8]"); + "Unexpected character ('}' (code 125)): expected a valid value (JSON String, Number, Array, Object or token 'null', 'true' or 'false')\n at [Source: (String)\"{ \"bad\": }\"; line: 1, column: 10]"); } @Test @@ -184,7 +190,7 @@ public void doesNotMatchWhenJsonPathWouldResolveToEmptyArray() { String json = "{\n" + " \"RequestDetail\" : {\n" + " \"ClientTag\" : \"test111\"\n" + " }\n" + "}"; - StringValuePattern pattern = WireMock.matchingJsonPath("$.RequestDetail.?(@=='test222')"); + StringValuePattern pattern = WireMock.matchingJsonPath("$.RequestDetail.[?(@=='test222')]"); MatchResult match = pattern.match(json); assertFalse(match.isExactMatch()); } diff --git a/src/test/java/com/github/tomakehurst/wiremock/matching/MatchesXPathPatternTest.java b/src/test/java/com/github/tomakehurst/wiremock/matching/MatchesXPathPatternTest.java index 730b00d84c..f50a7922c8 100644 --- a/src/test/java/com/github/tomakehurst/wiremock/matching/MatchesXPathPatternTest.java +++ b/src/test/java/com/github/tomakehurst/wiremock/matching/MatchesXPathPatternTest.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2016-2023 Thomas Akehurst + * Copyright (C) 2016-2024 Thomas Akehurst * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -320,7 +320,7 @@ void reportsErrorFromSubMatcher() { .match("{ bad json"); checkJsonError( matchResult, - "Unexpected character ('b' (code 98)): was expecting double-quote to start field name\n at [Source: (String)\"{ bad json\"; line: 1, column: 4]"); + "Unexpected character ('b' (code 98)): was expecting double-quote to start field name\n at [Source: (String)\"{ bad json\"; line: 1, column: 3]"); } @Test diff --git a/src/test/java/com/github/tomakehurst/wiremock/matching/MockRequest.java b/src/test/java/com/github/tomakehurst/wiremock/matching/MockRequest.java index 370233fc5a..ace8159e3f 100644 --- a/src/test/java/com/github/tomakehurst/wiremock/matching/MockRequest.java +++ b/src/test/java/com/github/tomakehurst/wiremock/matching/MockRequest.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2016-2023 Thomas Akehurst + * Copyright (C) 2016-2024 Thomas Akehurst * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -21,10 +21,10 @@ import static java.nio.charset.StandardCharsets.UTF_8; import static java.util.Arrays.asList; +import com.github.tomakehurst.wiremock.MultipartParserLoader; import com.github.tomakehurst.wiremock.common.Urls; import com.github.tomakehurst.wiremock.common.url.PathParams; import com.github.tomakehurst.wiremock.http.*; -import com.github.tomakehurst.wiremock.jetty11.MultipartParser; import com.github.tomakehurst.wiremock.verification.LoggedRequest; import java.net.URI; import java.util.*; @@ -292,7 +292,7 @@ public MockRequest multipartBody(String body) { contentTypeHeader.isPresent() ? contentTypeHeader.firstValue() : "multipart/form-data; boundary=BOUNDARY"; - this.multiparts = MultipartParser.parse(bytesFromString(body), contentType); + this.multiparts = MultipartParserLoader.parts(bytesFromString(body), contentType); return this; } diff --git a/src/test/java/com/github/tomakehurst/wiremock/matching/RequestPatternTest.java b/src/test/java/com/github/tomakehurst/wiremock/matching/RequestPatternTest.java index 73d4888806..de6107c791 100644 --- a/src/test/java/com/github/tomakehurst/wiremock/matching/RequestPatternTest.java +++ b/src/test/java/com/github/tomakehurst/wiremock/matching/RequestPatternTest.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2016-2023 Thomas Akehurst + * Copyright (C) 2016-2024 Thomas Akehurst * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -49,6 +49,7 @@ import com.github.tomakehurst.wiremock.common.Json; import com.github.tomakehurst.wiremock.http.FormParameter; import com.github.tomakehurst.wiremock.http.RequestMethod; +import com.github.tomakehurst.wiremock.junit5.EnabledIfJettyVersion; import java.util.HashMap; import java.util.List; import java.util.Map; @@ -394,6 +395,9 @@ public void doesNotMatchExactlyWhenOneBodyPatternDoesNotMatch() { assertFalse(matchResult.isExactMatch()); } + @EnabledIfJettyVersion( + major = 11, + reason = "Jetty 12 and above does not decode BASE64 content encoding") @Test public void matchesExactlyWith0DistanceWhenMultipartPatternsAllMatch() { RequestPattern requestPattern = @@ -476,6 +480,9 @@ public void doesNotMatchExactlyWhenOneMultipartHeaderPatternDoesNotMatch() { assertFalse(matchResult.isExactMatch()); } + @EnabledIfJettyVersion( + major = 11, + reason = "Jetty 12 and above does not decode BASE64 content encoding") @Test public void matchesExactlyWith0DistanceWhenAllMultipartPatternsMatchAllParts() { RequestPattern requestPattern = @@ -603,7 +610,7 @@ void doesNotMatchAnInvalidString(String toMatch) { requestPattern.match(mockRequest().method(GET).url("/foo/" + toMatch)); assertThat(matchResult.isExactMatch(), is(false)); - assertThat(matchResult.getDistance(), closeTo(0.02, 0.01)); + assertThat(matchResult.getDistance(), closeTo(0.1, 0.25)); } private static Stream invalidStrings() { diff --git a/src/test/java/com/github/tomakehurst/wiremock/standalone/CommandLineOptionsTest.java b/src/test/java/com/github/tomakehurst/wiremock/standalone/CommandLineOptionsTest.java index 2f2036c725..7dfc641b26 100644 --- a/src/test/java/com/github/tomakehurst/wiremock/standalone/CommandLineOptionsTest.java +++ b/src/test/java/com/github/tomakehurst/wiremock/standalone/CommandLineOptionsTest.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2011-2023 Thomas Akehurst + * Copyright (C) 2011-2024 Thomas Akehurst * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -103,6 +103,18 @@ public void disablesHttpWhenOptionPresentAndHttpsEnabled() { assertThat(options.getHttpDisabled(), is(true)); } + @Test + public void disablesHttp2PlainWhenOptionSet() { + CommandLineOptions options = new CommandLineOptions("--disable-http2-plain"); + assertThat(options.getHttp2PlainDisabled(), is(true)); + } + + @Test + public void disablesHttp2TlsWhenOptionSet() { + CommandLineOptions options = new CommandLineOptions("--disable-http2-tls"); + assertThat(options.getHttp2TlsDisabled(), is(true)); + } + @Test public void enablesHttpsAndSetsPortNumberWhenOptionPresent() { CommandLineOptions options = new CommandLineOptions("--https-port", "8443"); diff --git a/src/test/java/com/github/tomakehurst/wiremock/stubbing/ResponseDefinitionTest.java b/src/test/java/com/github/tomakehurst/wiremock/stubbing/ResponseDefinitionTest.java index 60e01db259..d89eae0b72 100644 --- a/src/test/java/com/github/tomakehurst/wiremock/stubbing/ResponseDefinitionTest.java +++ b/src/test/java/com/github/tomakehurst/wiremock/stubbing/ResponseDefinitionTest.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2012-2023 Thomas Akehurst + * Copyright (C) 2012-2024 Thomas Akehurst * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -47,6 +47,7 @@ public void copyProducesEqualObject() { "name.json", new HttpHeaders(httpHeader("thing", "thingvalue")), null, + null, 1112, null, null, diff --git a/src/test/java/com/github/tomakehurst/wiremock/stubbing/StubMappingJsonRecorderTest.java b/src/test/java/com/github/tomakehurst/wiremock/stubbing/StubMappingJsonRecorderTest.java index 69e39ab919..0e8771fd0d 100644 --- a/src/test/java/com/github/tomakehurst/wiremock/stubbing/StubMappingJsonRecorderTest.java +++ b/src/test/java/com/github/tomakehurst/wiremock/stubbing/StubMappingJsonRecorderTest.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2011-2023 Thomas Akehurst + * Copyright (C) 2011-2024 Thomas Akehurst * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -50,7 +50,7 @@ * @deprecated this test is for the legacy recorder which will be removed before 3.x is out of beta */ @Deprecated -public class StubMappingJsonRecorderTest { +class StubMappingJsonRecorderTest { private StubMappingJsonRecorder listener; @@ -94,7 +94,7 @@ private void constructRecordingListener(List headersToRecord) { + "} "; @Test - public void writesMappingFileAndCorrespondingBodyFileOnRequest() { + void writesMappingFileAndCorrespondingBodyFileOnRequest() { when(admin.countRequestsMatching((any(RequestPattern.class)))) .thenReturn(VerificationResult.withCount(0)); @@ -132,7 +132,7 @@ public void writesMappingFileAndCorrespondingBodyFileOnRequest() { + "} "; @Test - public void addsResponseHeaders() { + void addsResponseHeaders() { when(admin.countRequestsMatching((any(RequestPattern.class)))) .thenReturn(VerificationResult.withCount(0)); @@ -161,7 +161,7 @@ public void addsResponseHeaders() { } @Test - public void doesNotWriteFileIfRequestAlreadyReceived() { + void doesNotWriteFileIfRequestAlreadyReceived() { when(admin.countRequestsMatching((any(RequestPattern.class)))) .thenReturn(VerificationResult.withCount(1)); @@ -174,7 +174,7 @@ public void doesNotWriteFileIfRequestAlreadyReceived() { } @Test - public void doesNotWriteFileIfResponseNotFromProxy() { + void doesNotWriteFileIfResponseNotFromProxy() { when(admin.countRequestsMatching((any(RequestPattern.class)))) .thenReturn(VerificationResult.withCount(0)); @@ -204,7 +204,7 @@ public void doesNotWriteFileIfResponseNotFromProxy() { + "} "; @Test - public void includesBodyInRequestPatternIfInRequest() { + void includesBodyInRequestPatternIfInRequest() { when(admin.countRequestsMatching((any(RequestPattern.class)))) .thenReturn(VerificationResult.withCount(0)); @@ -261,7 +261,7 @@ public void includesBodyInRequestPatternIfInRequest() { new ArrayList<>(Collections.singletonList("Accept")); @Test - public void includesHeadersInRequestPatternIfHeaderMatchingEnabled() { + void includesHeadersInRequestPatternIfHeaderMatchingEnabled() { constructRecordingListener(MATCHING_REQUEST_HEADERS); when(admin.countRequestsMatching((any(RequestPattern.class)))) @@ -314,7 +314,7 @@ public void includesHeadersInRequestPatternIfHeaderMatchingEnabled() { + "}"; @Test - public void matchesBodyOnEqualToJsonIfJsonInRequestContentTypeHeader() { + void matchesBodyOnEqualToJsonIfJsonInRequestContentTypeHeader() { when(admin.countRequestsMatching((any(RequestPattern.class)))) .thenReturn(VerificationResult.withCount(0)); @@ -350,7 +350,7 @@ public void matchesBodyOnEqualToJsonIfJsonInRequestContentTypeHeader() { + "}"; @Test - public void matchesBodyOnEqualToXmlIfXmlInRequestContentTypeHeader() { + void matchesBodyOnEqualToXmlIfXmlInRequestContentTypeHeader() { when(admin.countRequestsMatching((any(RequestPattern.class)))) .thenReturn(VerificationResult.withCount(0)); Request request = @@ -385,7 +385,7 @@ public void matchesBodyOnEqualToXmlIfXmlInRequestContentTypeHeader() { + "} "; @Test - public void decompressesGzippedResponseBodyAndRemovesContentEncodingHeader() { + void decompressesGzippedResponseBodyAndRemovesContentEncodingHeader() { when(admin.countRequestsMatching((any(RequestPattern.class)))) .thenReturn(VerificationResult.withCount(0)); @@ -417,12 +417,11 @@ public void decompressesGzippedResponseBodyAndRemovesContentEncodingHeader() { } private static final String MULTIPART_REQUEST_MAPPING = - "{ \n" - + " \"id\": \"41544750-0c69-3fd7-93b1-f79499f987c3\", \n" - + " \"uuid\": \"41544750-0c69-3fd7-93b1-f79499f987c3\", \n" - + " \"request\": { \n" - + " \"method\": \"POST\", \n" - + " \"url\": \"/multipart/content\", \n" + "{ \n" + + " \"id\" : \"41544750-0c69-3fd7-93b1-f79499f987c3\", \n" + + " \"request\" : { \n" + + " \"url\" : \"/multipart/content\", \n" + + " \"method\" : \"POST\", \n" + " \"multipartPatterns\" : [ { \n" + " \"name\" : \"binaryFile\", \n" + " \"matchingType\" : \"ALL\", \n" @@ -451,7 +450,7 @@ public void decompressesGzippedResponseBodyAndRemovesContentEncodingHeader() { + " \"headers\" : { \n" + " \"Content-Disposition\" : { \n" + " \"contains\" : \"name=\\\"formInput\\\"\" \n" - + " }, \n" + + " } \n" + " }, \n" + " \"bodyPatterns\" : [ { \n" + " \"equalTo\" : \"I am a field!\" \n" @@ -461,11 +460,12 @@ public void decompressesGzippedResponseBodyAndRemovesContentEncodingHeader() { + " \"response\": { \n" + " \"status\": 200, \n" + " \"bodyFileName\": \"body-multipart-content-1$2!3.txt\" \n" - + " } \n" + + " }, \n" + + " \"uuid\": \"41544750-0c69-3fd7-93b1-f79499f987c3\" \n" + "} "; @Test - public void multipartRequestProcessing() { + void multipartRequestProcessing() { when(admin.countRequestsMatching((any(RequestPattern.class)))) .thenReturn(VerificationResult.withCount(0)); @@ -510,7 +510,7 @@ public void multipartRequestProcessing() { + "}"; @Test - public void multipartRequestProcessingWithNonFileMultipart() { + void multipartRequestProcessingWithNonFileMultipart() { when(admin.countRequestsMatching((any(RequestPattern.class)))) .thenReturn(VerificationResult.withCount(0)); @@ -534,42 +534,42 @@ public void multipartRequestProcessingWithNonFileMultipart() { } @Test - public void detectsJsonExtensionFromFileExtension() { + void detectsJsonExtensionFromFileExtension() { assertResultingFileExtension("/my/file.json", "json"); } @Test - public void detectsGifExtensionFromFileExtension() { + void detectsGifExtensionFromFileExtension() { assertResultingFileExtension("/my/file.gif", "gif"); } @Test - public void detectsXmlExtensionFromResponseContentTypeHeader() { + void detectsXmlExtensionFromResponseContentTypeHeader() { assertResultingFileExtension("/noext", "xml", "application/xml"); } @Test - public void detectsJsonExtensionFromResponseContentTypeHeader() { + void detectsJsonExtensionFromResponseContentTypeHeader() { assertResultingFileExtension("/noext", "json", "application/json"); } @Test - public void detectsJsonExtensionFromCustomResponseContentTypeHeader() { + void detectsJsonExtensionFromCustomResponseContentTypeHeader() { assertResultingFileExtension("/noext", "json", "application/vnd.api+json"); } @Test - public void detectsJpegExtensionFromResponseContentTypeHeader() { + void detectsJpegExtensionFromResponseContentTypeHeader() { assertResultingFileExtension("/noext", "jpeg", "image/jpeg"); } @Test - public void detectsIcoExtensionFromResponseContentTypeHeader() { + void detectsIcoExtensionFromResponseContentTypeHeader() { assertResultingFileExtension("/noext", "ico", "image/x-icon"); } @Test - public void sanitisesFilenamesBySwappingSymbolsForUnderscores() { + void sanitisesFilenamesBySwappingSymbolsForUnderscores() { when(admin.countRequestsMatching((any(RequestPattern.class)))) .thenReturn(VerificationResult.withCount(0)); diff --git a/src/test/java/com/github/tomakehurst/wiremock/verification/LoggedRequestTest.java b/src/test/java/com/github/tomakehurst/wiremock/verification/LoggedRequestTest.java index 7fdad4e69d..685ba6e9b7 100644 --- a/src/test/java/com/github/tomakehurst/wiremock/verification/LoggedRequestTest.java +++ b/src/test/java/com/github/tomakehurst/wiremock/verification/LoggedRequestTest.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2012-2023 Thomas Akehurst + * Copyright (C) 2012-2024 Thomas Akehurst * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -34,23 +34,17 @@ import java.util.Date; import java.util.List; import java.util.Map; -import java.util.TimeZone; -import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.skyscreamer.jsonassert.JSONAssert; +@SuppressWarnings("rawtypes") public class LoggedRequestTest { public static final String REQUEST_BODY = "some text 形声字形聲字"; public static final String REQUEST_BODY_AS_BASE64 = "c29tZSB0ZXh0IOW9ouWjsOWtl+W9ouiBsuWtlw=="; - @BeforeEach - public void init() { - System.out.println(TimeZone.getDefault()); - } - @Test - public void headerMatchingIsCaseInsensitive() { + void headerMatchingIsCaseInsensitive() { LoggedRequest loggedRequest = createFrom( aRequest() @@ -93,13 +87,19 @@ public void headerMatchingIsCaseInsensitive() { + " \"body\" : \"" + REQUEST_BODY + "\",\n" + + " \"protocol\" : \"HTTP/1.1\",\n" + + " \"scheme\" : \"http\",\n" + + " \"host\" : \"mydomain.com\",\n" + + " \"port\" : 80,\n" + " \"loggedDateString\" : \"" + DATE + "\",\n" + + " \"queryParams\" : { },\n" + + " \"formParams\" : { }\n" + " }"; @Test - public void jsonRepresentation() throws Exception { + void jsonRepresentation() throws Exception { HttpHeaders headers = new HttpHeaders(httpHeader("Accept-Language", "en-us,en;q=0.5")); Map cookies = Map.of("first_cookie", new Cookie("yum"), "monster_cookie", new Cookie("COOKIIIEESS")); @@ -127,7 +127,7 @@ public void jsonRepresentation() throws Exception { } @Test - public void bodyEncodedAsUTF8() throws Exception { + void bodyEncodedAsUTF8() { LoggedRequest loggedRequest = new LoggedRequest( "/my/url", @@ -168,7 +168,7 @@ public void bodyEncodedAsUTF8() throws Exception { + "}"; @Test - public void queryParametersAreSerialized() { + void queryParametersAreSerialized() { LoggedRequest req = new LoggedRequest( "/sample/path?test-param-1=value-1&test-param-2=value-2", @@ -198,7 +198,7 @@ public void queryParametersAreSerialized() { } @Test - public void queryParametersAreDeserialized() throws IOException { + void queryParametersAreDeserialized() throws IOException { LoggedRequest req = new ObjectMapper().readValue(JSON_PARAMS_EXAMPLE, LoggedRequest.class); assertEquals("test-param-1", req.queryParameter("test-param-1").key()); diff --git a/src/test/java/com/github/tomakehurst/wiremock/verification/diff/PlainTextDiffRendererTest.java b/src/test/java/com/github/tomakehurst/wiremock/verification/diff/PlainTextDiffRendererTest.java index 32f70bb25a..cd3d90cc64 100644 --- a/src/test/java/com/github/tomakehurst/wiremock/verification/diff/PlainTextDiffRendererTest.java +++ b/src/test/java/com/github/tomakehurst/wiremock/verification/diff/PlainTextDiffRendererTest.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2017-2023 Thomas Akehurst + * Copyright (C) 2017-2024 Thomas Akehurst * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -38,7 +38,6 @@ import java.util.HashMap; import java.util.List; import java.util.Map; -import org.apache.commons.lang3.SystemUtils; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.condition.DisabledForJreRange; @@ -211,7 +210,7 @@ public void wrapsLargeJsonBodiesAppropriately() { // Ugh. The joys of Microsoft's line ending innovations. String expected = - SystemUtils.IS_OS_WINDOWS + System.getProperty("os.name").startsWith("Windows") ? file("not-found-diff-sample_large_json_windows.txt") : file("not-found-diff-sample_large_json.txt"); diff --git a/src/test/resources/META-INF/services/com.github.tomakehurst.wiremock.MultipartParserLoader b/src/test/resources/META-INF/services/com.github.tomakehurst.wiremock.MultipartParserLoader new file mode 100644 index 0000000000..2cbf46bd51 --- /dev/null +++ b/src/test/resources/META-INF/services/com.github.tomakehurst.wiremock.MultipartParserLoader @@ -0,0 +1,2 @@ +com.github.tomakehurst.wiremock.jetty11.MultipartParserLoader +com.github.tomakehurst.wiremock.jetty12.MultipartParserLoader \ No newline at end of file diff --git a/src/test/resources/frozen/no-standard-streams b/src/test/resources/frozen/no-standard-streams new file mode 100644 index 0000000000..55b7c7ba2c --- /dev/null +++ b/src/test/resources/frozen/no-standard-streams @@ -0,0 +1 @@ +Method gets field in (ExtensionDeclarations.java:79) \ No newline at end of file diff --git a/src/test/resources/frozen/stored.rules b/src/test/resources/frozen/stored.rules index 37e20df19e..fd6ecdd810 100644 --- a/src/test/resources/frozen/stored.rules +++ b/src/test/resources/frozen/stored.rules @@ -1,3 +1,5 @@ -#Fri Oct 15 19:25:52 AEDT 2021 +# +#Tue Mar 26 06:43:05 AEDT 2024 no\ classes\ should\ throw\ generic\ exceptions=do-not-throw-generic-exception -should\ use\ all\ methods,\ because\ unused\ methods\ should\ be\ removed=unused-methods \ No newline at end of file +classes\ should\ not\ access\ standard\ streams=no-standard-streams +should\ use\ all\ methods,\ because\ unused\ methods\ should\ be\ removed=unused-methods diff --git a/src/test/resources/logback.xml b/src/test/resources/logback.xml index 589bcff566..c140f00b73 100644 --- a/src/test/resources/logback.xml +++ b/src/test/resources/logback.xml @@ -6,11 +6,11 @@ - + - + diff --git a/src/test/resources/wiremock-gui-version.properties b/src/test/resources/wiremock-gui-version.properties new file mode 100644 index 0000000000..71bed61e33 --- /dev/null +++ b/src/test/resources/wiremock-gui-version.properties @@ -0,0 +1,3 @@ +# version file +gui-version=X.X.X +version=X.X.X diff --git a/src/testFixtures/java/com/github/tomakehurst/wiremock/MultipartParserLoader.java b/src/testFixtures/java/com/github/tomakehurst/wiremock/MultipartParserLoader.java new file mode 100644 index 0000000000..fcdfd2e649 --- /dev/null +++ b/src/testFixtures/java/com/github/tomakehurst/wiremock/MultipartParserLoader.java @@ -0,0 +1,89 @@ +/* + * Copyright (C) 2024 Thomas Akehurst + * + * 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 + * + * http://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. + */ +package com.github.tomakehurst.wiremock; + +import com.github.tomakehurst.wiremock.http.Request; +import java.util.Collection; +import java.util.Optional; +import java.util.ServiceConfigurationError; +import java.util.ServiceLoader; +import org.eclipse.jetty.util.Jetty; + +/** + * Create the Jetty's version-dependent {@link MultipartParser} instance that accepts the Jetty's + * major version into account while creating the multipart parser instance. + */ +public interface MultipartParserLoader { + Optional getMultipartParser(String jettyMajorVersion); + + /** The multipart parser implementation that depends on Jetty version being used. */ + interface MultipartParser { + Collection parse(byte[] body, String contentType); + } + + /** + * Parses the body using {@link MultipartParser} instance. + * + * @param body body + * @param contentType content type + * @return the list of parsed multipart parts + */ + static Collection parts(byte[] body, String contentType) { + return create(Jetty.VERSION).parse(body, contentType); + } + + /** + * Create the Jetty's version-dependent {@link MultipartParser} instance using Java's {@link + * ServiceLoader} mechanism or throws an exception if none of the multipart parser could be + * created. + * + * @param jettyVersion Jetty version at runtime + * @return {@link MultipartParser} instance + */ + static MultipartParser create(String jettyVersion) { + final String[] version = Jetty.VERSION.split("[.]"); + if (version.length == 0 || version[0].isBlank()) { + throw new IllegalArgumentException( + "Unrecognized Jetty version: " + + jettyVersion + + ". Please make sure the right Jetty dependencies are on the classpath."); + } + + final String jettyMajorVersion = version[0]; + final ServiceLoader loaders = + ServiceLoader.load(MultipartParserLoader.class); + + Throwable cause = null; + try { + for (final MultipartParserLoader loader : loaders) { + final Optional multipartParserOpt = + loader.getMultipartParser(jettyMajorVersion); + if (multipartParserOpt.isPresent()) { + return multipartParserOpt.get(); + } + } + } catch (final ServiceConfigurationError ex) { + /* only catch this kind of exception, the Jetty's HttpServerFactoryLoader could not be instantiated */ + cause = ex; + } + + throw new IllegalStateException( + "Unable to find MultipartParserLoader for Jetty version " + + jettyMajorVersion + + " (only Jetty 11/12 are supported at the moment). Please make sure the right Jetty dependencies are on the classpath.", + cause); + } +} diff --git a/src/test/java/com/github/tomakehurst/wiremock/testsupport/Assumptions.java b/src/testFixtures/java/com/github/tomakehurst/wiremock/testsupport/Assumptions.java similarity index 80% rename from src/test/java/com/github/tomakehurst/wiremock/testsupport/Assumptions.java rename to src/testFixtures/java/com/github/tomakehurst/wiremock/testsupport/Assumptions.java index 62d2f0d3a9..066111afbf 100644 --- a/src/test/java/com/github/tomakehurst/wiremock/testsupport/Assumptions.java +++ b/src/testFixtures/java/com/github/tomakehurst/wiremock/testsupport/Assumptions.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2021 Thomas Akehurst + * Copyright (C) 2021-2024 Thomas Akehurst * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -17,11 +17,11 @@ import static org.junit.jupiter.api.Assumptions.assumeFalse; -import org.apache.commons.lang3.SystemUtils; - public class Assumptions { public static void doNotRunOnMacOSXInCI() { - assumeFalse(SystemUtils.IS_OS_MAC_OSX && "true".equalsIgnoreCase(System.getenv("CI"))); + assumeFalse( + System.getProperty("os.name").startsWith("Mac OS X") + && "true".equalsIgnoreCase(System.getenv("CI"))); } } diff --git a/src/test/java/com/github/tomakehurst/wiremock/testsupport/CompositeNotifier.java b/src/testFixtures/java/com/github/tomakehurst/wiremock/testsupport/CompositeNotifier.java similarity index 96% rename from src/test/java/com/github/tomakehurst/wiremock/testsupport/CompositeNotifier.java rename to src/testFixtures/java/com/github/tomakehurst/wiremock/testsupport/CompositeNotifier.java index 9e63c4f26a..7ea143b5ed 100644 --- a/src/test/java/com/github/tomakehurst/wiremock/testsupport/CompositeNotifier.java +++ b/src/testFixtures/java/com/github/tomakehurst/wiremock/testsupport/CompositeNotifier.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2021-2023 Thomas Akehurst + * Copyright (C) 2021-2024 Thomas Akehurst * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/src/test/java/com/github/tomakehurst/wiremock/testsupport/ConstantHttpHeaderWebhookTransformer.java b/src/testFixtures/java/com/github/tomakehurst/wiremock/testsupport/ConstantHttpHeaderWebhookTransformer.java similarity index 96% rename from src/test/java/com/github/tomakehurst/wiremock/testsupport/ConstantHttpHeaderWebhookTransformer.java rename to src/testFixtures/java/com/github/tomakehurst/wiremock/testsupport/ConstantHttpHeaderWebhookTransformer.java index 2b081a60fc..2080dc3d6b 100644 --- a/src/test/java/com/github/tomakehurst/wiremock/testsupport/ConstantHttpHeaderWebhookTransformer.java +++ b/src/testFixtures/java/com/github/tomakehurst/wiremock/testsupport/ConstantHttpHeaderWebhookTransformer.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2021-2023 Thomas Akehurst + * Copyright (C) 2021-2024 Thomas Akehurst * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/src/test/java/com/github/tomakehurst/wiremock/testsupport/ExtensionFactoryUtils.java b/src/testFixtures/java/com/github/tomakehurst/wiremock/testsupport/ExtensionFactoryUtils.java similarity index 90% rename from src/test/java/com/github/tomakehurst/wiremock/testsupport/ExtensionFactoryUtils.java rename to src/testFixtures/java/com/github/tomakehurst/wiremock/testsupport/ExtensionFactoryUtils.java index 013f229ec9..9648abe610 100644 --- a/src/test/java/com/github/tomakehurst/wiremock/testsupport/ExtensionFactoryUtils.java +++ b/src/testFixtures/java/com/github/tomakehurst/wiremock/testsupport/ExtensionFactoryUtils.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2023 Thomas Akehurst + * Copyright (C) 2023-2024 Thomas Akehurst * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -23,7 +23,6 @@ import com.github.tomakehurst.wiremock.extension.Extension; import com.github.tomakehurst.wiremock.extension.ExtensionFactory; import com.github.tomakehurst.wiremock.extension.responsetemplating.ResponseTemplateTransformer; -import com.github.tomakehurst.wiremock.extension.responsetemplating.ResponseTemplateTransformerTest; import java.util.List; import java.util.Map; @@ -64,10 +63,7 @@ public static Extension buildExtension( MockWireMockServices wireMockServices, ExtensionFactory factory) { FileSource fileSource = new ClasspathFileSource( - ResponseTemplateTransformerTest.class - .getClassLoader() - .getResource("templates") - .getPath()); + ExtensionFactoryUtils.class.getClassLoader().getResource("templates").getPath()); wireMockServices.setFileSource(fileSource); return factory.create(wireMockServices).stream().findFirst().get(); } diff --git a/src/test/java/com/github/tomakehurst/wiremock/testsupport/GlobalStubMappingTransformer.java b/src/testFixtures/java/com/github/tomakehurst/wiremock/testsupport/GlobalStubMappingTransformer.java similarity index 96% rename from src/test/java/com/github/tomakehurst/wiremock/testsupport/GlobalStubMappingTransformer.java rename to src/testFixtures/java/com/github/tomakehurst/wiremock/testsupport/GlobalStubMappingTransformer.java index f475d44fe6..8ed439b5ad 100644 --- a/src/test/java/com/github/tomakehurst/wiremock/testsupport/GlobalStubMappingTransformer.java +++ b/src/testFixtures/java/com/github/tomakehurst/wiremock/testsupport/GlobalStubMappingTransformer.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2017-2021 Thomas Akehurst + * Copyright (C) 2017-2024 Thomas Akehurst * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/src/test/java/com/github/tomakehurst/wiremock/testsupport/MappingJsonSamples.java b/src/testFixtures/java/com/github/tomakehurst/wiremock/testsupport/MappingJsonSamples.java similarity index 100% rename from src/test/java/com/github/tomakehurst/wiremock/testsupport/MappingJsonSamples.java rename to src/testFixtures/java/com/github/tomakehurst/wiremock/testsupport/MappingJsonSamples.java diff --git a/src/test/java/com/github/tomakehurst/wiremock/testsupport/MockHttpResponder.java b/src/testFixtures/java/com/github/tomakehurst/wiremock/testsupport/MockHttpResponder.java similarity index 96% rename from src/test/java/com/github/tomakehurst/wiremock/testsupport/MockHttpResponder.java rename to src/testFixtures/java/com/github/tomakehurst/wiremock/testsupport/MockHttpResponder.java index e72770bcfe..9b0783e4e8 100644 --- a/src/test/java/com/github/tomakehurst/wiremock/testsupport/MockHttpResponder.java +++ b/src/testFixtures/java/com/github/tomakehurst/wiremock/testsupport/MockHttpResponder.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2016-2023 Thomas Akehurst + * Copyright (C) 2016-2024 Thomas Akehurst * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/src/test/java/com/github/tomakehurst/wiremock/testsupport/MockHttpServletRequest.java b/src/testFixtures/java/com/github/tomakehurst/wiremock/testsupport/MockHttpServletRequest.java similarity index 92% rename from src/test/java/com/github/tomakehurst/wiremock/testsupport/MockHttpServletRequest.java rename to src/testFixtures/java/com/github/tomakehurst/wiremock/testsupport/MockHttpServletRequest.java index c08c5aca31..3ec2df32d9 100644 --- a/src/test/java/com/github/tomakehurst/wiremock/testsupport/MockHttpServletRequest.java +++ b/src/testFixtures/java/com/github/tomakehurst/wiremock/testsupport/MockHttpServletRequest.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2012-2022 Thomas Akehurst + * Copyright (C) 2012-2024 Thomas Akehurst * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -15,20 +15,8 @@ */ package com.github.tomakehurst.wiremock.testsupport; -import jakarta.servlet.AsyncContext; -import jakarta.servlet.DispatcherType; -import jakarta.servlet.RequestDispatcher; -import jakarta.servlet.ServletContext; -import jakarta.servlet.ServletException; -import jakarta.servlet.ServletInputStream; -import jakarta.servlet.ServletRequest; -import jakarta.servlet.ServletResponse; -import jakarta.servlet.http.Cookie; -import jakarta.servlet.http.HttpServletRequest; -import jakarta.servlet.http.HttpServletResponse; -import jakarta.servlet.http.HttpSession; -import jakarta.servlet.http.HttpUpgradeHandler; -import jakarta.servlet.http.Part; +import jakarta.servlet.*; +import jakarta.servlet.http.*; import java.io.BufferedReader; import java.io.IOException; import java.io.UnsupportedEncodingException; diff --git a/src/test/java/com/github/tomakehurst/wiremock/testsupport/MockRequestBuilder.java b/src/testFixtures/java/com/github/tomakehurst/wiremock/testsupport/MockRequestBuilder.java similarity index 99% rename from src/test/java/com/github/tomakehurst/wiremock/testsupport/MockRequestBuilder.java rename to src/testFixtures/java/com/github/tomakehurst/wiremock/testsupport/MockRequestBuilder.java index 0f1b0d3e5a..ca37092f2a 100644 --- a/src/test/java/com/github/tomakehurst/wiremock/testsupport/MockRequestBuilder.java +++ b/src/testFixtures/java/com/github/tomakehurst/wiremock/testsupport/MockRequestBuilder.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2011-2023 Thomas Akehurst + * Copyright (C) 2011-2024 Thomas Akehurst * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/src/test/java/com/github/tomakehurst/wiremock/testsupport/MockWireMockServices.java b/src/testFixtures/java/com/github/tomakehurst/wiremock/testsupport/MockWireMockServices.java similarity index 98% rename from src/test/java/com/github/tomakehurst/wiremock/testsupport/MockWireMockServices.java rename to src/testFixtures/java/com/github/tomakehurst/wiremock/testsupport/MockWireMockServices.java index 7ca181ca94..c2dc05e25e 100644 --- a/src/test/java/com/github/tomakehurst/wiremock/testsupport/MockWireMockServices.java +++ b/src/testFixtures/java/com/github/tomakehurst/wiremock/testsupport/MockWireMockServices.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2023 Thomas Akehurst + * Copyright (C) 2023-2024 Thomas Akehurst * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/src/test/java/com/github/tomakehurst/wiremock/testsupport/MultipartBody.java b/src/testFixtures/java/com/github/tomakehurst/wiremock/testsupport/MultipartBody.java similarity index 97% rename from src/test/java/com/github/tomakehurst/wiremock/testsupport/MultipartBody.java rename to src/testFixtures/java/com/github/tomakehurst/wiremock/testsupport/MultipartBody.java index 9f0fd60b49..dbeab91593 100644 --- a/src/test/java/com/github/tomakehurst/wiremock/testsupport/MultipartBody.java +++ b/src/testFixtures/java/com/github/tomakehurst/wiremock/testsupport/MultipartBody.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2017-2021 Thomas Akehurst + * Copyright (C) 2017-2024 Thomas Akehurst * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/src/test/java/com/github/tomakehurst/wiremock/testsupport/Network.java b/src/testFixtures/java/com/github/tomakehurst/wiremock/testsupport/Network.java similarity index 95% rename from src/test/java/com/github/tomakehurst/wiremock/testsupport/Network.java rename to src/testFixtures/java/com/github/tomakehurst/wiremock/testsupport/Network.java index 4e982c8a5c..1bd3c5a930 100644 --- a/src/test/java/com/github/tomakehurst/wiremock/testsupport/Network.java +++ b/src/testFixtures/java/com/github/tomakehurst/wiremock/testsupport/Network.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2015-2021 Thomas Akehurst + * Copyright (C) 2015-2024 Thomas Akehurst * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/src/test/java/com/github/tomakehurst/wiremock/testsupport/NoFileSource.java b/src/testFixtures/java/com/github/tomakehurst/wiremock/testsupport/NoFileSource.java similarity index 97% rename from src/test/java/com/github/tomakehurst/wiremock/testsupport/NoFileSource.java rename to src/testFixtures/java/com/github/tomakehurst/wiremock/testsupport/NoFileSource.java index 6dc1bce8c4..41e760b758 100644 --- a/src/test/java/com/github/tomakehurst/wiremock/testsupport/NoFileSource.java +++ b/src/testFixtures/java/com/github/tomakehurst/wiremock/testsupport/NoFileSource.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2016-2022 Thomas Akehurst + * Copyright (C) 2016-2024 Thomas Akehurst * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/src/test/java/com/github/tomakehurst/wiremock/testsupport/NonGlobalStubMappingTransformer.java b/src/testFixtures/java/com/github/tomakehurst/wiremock/testsupport/NonGlobalStubMappingTransformer.java similarity index 97% rename from src/test/java/com/github/tomakehurst/wiremock/testsupport/NonGlobalStubMappingTransformer.java rename to src/testFixtures/java/com/github/tomakehurst/wiremock/testsupport/NonGlobalStubMappingTransformer.java index b7cc7d8007..ae131b904d 100644 --- a/src/test/java/com/github/tomakehurst/wiremock/testsupport/NonGlobalStubMappingTransformer.java +++ b/src/testFixtures/java/com/github/tomakehurst/wiremock/testsupport/NonGlobalStubMappingTransformer.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2017-2021 Thomas Akehurst + * Copyright (C) 2017-2024 Thomas Akehurst * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/src/test/java/com/github/tomakehurst/wiremock/testsupport/ServeEventChecks.java b/src/testFixtures/java/com/github/tomakehurst/wiremock/testsupport/ServeEventChecks.java similarity index 98% rename from src/test/java/com/github/tomakehurst/wiremock/testsupport/ServeEventChecks.java rename to src/testFixtures/java/com/github/tomakehurst/wiremock/testsupport/ServeEventChecks.java index fabc06ad5b..600c37eb23 100644 --- a/src/test/java/com/github/tomakehurst/wiremock/testsupport/ServeEventChecks.java +++ b/src/testFixtures/java/com/github/tomakehurst/wiremock/testsupport/ServeEventChecks.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2023 Thomas Akehurst + * Copyright (C) 2023-2024 Thomas Akehurst * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/src/test/java/com/github/tomakehurst/wiremock/testsupport/TestFiles.java b/src/testFixtures/java/com/github/tomakehurst/wiremock/testsupport/TestFiles.java similarity index 91% rename from src/test/java/com/github/tomakehurst/wiremock/testsupport/TestFiles.java rename to src/testFixtures/java/com/github/tomakehurst/wiremock/testsupport/TestFiles.java index e1b7293141..be6edbecfd 100644 --- a/src/test/java/com/github/tomakehurst/wiremock/testsupport/TestFiles.java +++ b/src/testFixtures/java/com/github/tomakehurst/wiremock/testsupport/TestFiles.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2017-2023 Thomas Akehurst + * Copyright (C) 2017-2024 Thomas Akehurst * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,13 +16,13 @@ package com.github.tomakehurst.wiremock.testsupport; import static com.github.tomakehurst.wiremock.common.Exceptions.throwUnchecked; -import static com.github.tomakehurst.wiremock.common.ResourceUtil.*; +import static com.github.tomakehurst.wiremock.common.ResourceUtil.getResourcePath; +import static com.github.tomakehurst.wiremock.common.ResourceUtil.getResourceURI; import static java.nio.charset.StandardCharsets.UTF_8; import java.io.File; import java.io.IOException; import java.nio.file.Files; -import org.apache.commons.lang3.SystemUtils; public class TestFiles { @@ -46,7 +46,7 @@ public static String defaultTestFilesRoot() { public static String file(String path) { try { String text = Files.readString(getResourcePath(TestFiles.class, path), UTF_8); - if (SystemUtils.IS_OS_WINDOWS) { + if (System.getProperty("os.name").startsWith("Windows")) { text = text.replaceAll("\\r\\n", "\n").replaceAll("\\r", "\n"); } diff --git a/src/test/java/com/github/tomakehurst/wiremock/testsupport/TestHttpHeader.java b/src/testFixtures/java/com/github/tomakehurst/wiremock/testsupport/TestHttpHeader.java similarity index 95% rename from src/test/java/com/github/tomakehurst/wiremock/testsupport/TestHttpHeader.java rename to src/testFixtures/java/com/github/tomakehurst/wiremock/testsupport/TestHttpHeader.java index e40aef8064..185ce47072 100644 --- a/src/test/java/com/github/tomakehurst/wiremock/testsupport/TestHttpHeader.java +++ b/src/testFixtures/java/com/github/tomakehurst/wiremock/testsupport/TestHttpHeader.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2011-2021 Thomas Akehurst + * Copyright (C) 2011-2024 Thomas Akehurst * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/src/test/java/com/github/tomakehurst/wiremock/testsupport/TestNotifier.java b/src/testFixtures/java/com/github/tomakehurst/wiremock/testsupport/TestNotifier.java similarity index 98% rename from src/test/java/com/github/tomakehurst/wiremock/testsupport/TestNotifier.java rename to src/testFixtures/java/com/github/tomakehurst/wiremock/testsupport/TestNotifier.java index 14e02f19dc..81284bafe0 100644 --- a/src/test/java/com/github/tomakehurst/wiremock/testsupport/TestNotifier.java +++ b/src/testFixtures/java/com/github/tomakehurst/wiremock/testsupport/TestNotifier.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2016-2023 Thomas Akehurst + * Copyright (C) 2016-2024 Thomas Akehurst * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/src/test/java/com/github/tomakehurst/wiremock/testsupport/ThrowingWebhookTransformer.java b/src/testFixtures/java/com/github/tomakehurst/wiremock/testsupport/ThrowingWebhookTransformer.java similarity index 95% rename from src/test/java/com/github/tomakehurst/wiremock/testsupport/ThrowingWebhookTransformer.java rename to src/testFixtures/java/com/github/tomakehurst/wiremock/testsupport/ThrowingWebhookTransformer.java index a4a0ee6e98..c0b8a77c78 100644 --- a/src/test/java/com/github/tomakehurst/wiremock/testsupport/ThrowingWebhookTransformer.java +++ b/src/testFixtures/java/com/github/tomakehurst/wiremock/testsupport/ThrowingWebhookTransformer.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2023 Thomas Akehurst + * Copyright (C) 2023-2024 Thomas Akehurst * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/src/test/java/com/github/tomakehurst/wiremock/testsupport/WireMatchers.java b/src/testFixtures/java/com/github/tomakehurst/wiremock/testsupport/WireMatchers.java similarity index 98% rename from src/test/java/com/github/tomakehurst/wiremock/testsupport/WireMatchers.java rename to src/testFixtures/java/com/github/tomakehurst/wiremock/testsupport/WireMatchers.java index 7e7f8a556f..30ee8ea0fb 100644 --- a/src/test/java/com/github/tomakehurst/wiremock/testsupport/WireMatchers.java +++ b/src/testFixtures/java/com/github/tomakehurst/wiremock/testsupport/WireMatchers.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2011-2023 Thomas Akehurst + * Copyright (C) 2011-2024 Thomas Akehurst * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -33,12 +33,7 @@ import java.nio.file.Path; import java.text.ParseException; import java.text.SimpleDateFormat; -import java.util.Arrays; -import java.util.Date; -import java.util.Iterator; -import java.util.List; -import java.util.NoSuchElementException; -import java.util.Objects; +import java.util.*; import java.util.function.Predicate; import java.util.regex.Pattern; import java.util.stream.Collectors; diff --git a/src/test/java/com/github/tomakehurst/wiremock/testsupport/WireMockResponse.java b/src/testFixtures/java/com/github/tomakehurst/wiremock/testsupport/WireMockResponse.java similarity index 98% rename from src/test/java/com/github/tomakehurst/wiremock/testsupport/WireMockResponse.java rename to src/testFixtures/java/com/github/tomakehurst/wiremock/testsupport/WireMockResponse.java index c4d70c2b4a..ad60db7090 100644 --- a/src/test/java/com/github/tomakehurst/wiremock/testsupport/WireMockResponse.java +++ b/src/testFixtures/java/com/github/tomakehurst/wiremock/testsupport/WireMockResponse.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2011-2023 Thomas Akehurst + * Copyright (C) 2011-2024 Thomas Akehurst * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/src/test/java/com/github/tomakehurst/wiremock/testsupport/WireMockTestClient.java b/src/testFixtures/java/com/github/tomakehurst/wiremock/testsupport/WireMockTestClient.java similarity index 100% rename from src/test/java/com/github/tomakehurst/wiremock/testsupport/WireMockTestClient.java rename to src/testFixtures/java/com/github/tomakehurst/wiremock/testsupport/WireMockTestClient.java diff --git a/ui/package.json b/ui/package.json index c936c0c522..cb06c62181 100644 --- a/ui/package.json +++ b/ui/package.json @@ -1,6 +1,6 @@ { "name": "wiremock-ui-resources", - "version": "3.4.2", + "version": "3.5.2", "description": "WireMock UI resources processor", "engines": { "node": ">= 0.10.0" diff --git a/webapp/wiremock/src/app/components/home/home.component.html b/webapp/wiremock/src/app/components/home/home.component.html index 20dabce5b9..7fc09fa4f8 100644 --- a/webapp/wiremock/src/app/components/home/home.component.html +++ b/webapp/wiremock/src/app/components/home/home.component.html @@ -114,6 +114,7 @@ + diff --git a/webapp/wiremock/src/app/components/home/home.component.ts b/webapp/wiremock/src/app/components/home/home.component.ts index a43b0e8bf2..ae622e9c7c 100644 --- a/webapp/wiremock/src/app/components/home/home.component.ts +++ b/webapp/wiremock/src/app/components/home/home.component.ts @@ -30,7 +30,7 @@ export class HomeComponent implements OnInit, OnDestroy { currentRecordingStatus?: RecordingStatus; version?: string; - versionTooltip?: string; + guiVersion?: string; RecordingStatus = RecordingStatus; @@ -83,6 +83,7 @@ export class HomeComponent implements OnInit, OnDestroy { this.wiremockService.getVersion().subscribe({ next: version => { this.version = `Version: ${version.version}`; + this.guiVersion = `GUI Version: ${version.guiVersion}`; }, }); } diff --git a/webapp/wiremock/src/app/model/wiremock/version.ts b/webapp/wiremock/src/app/model/wiremock/version.ts index 8740ae545f..d07c779b6f 100644 --- a/webapp/wiremock/src/app/model/wiremock/version.ts +++ b/webapp/wiremock/src/app/model/wiremock/version.ts @@ -1,7 +1,9 @@ export class Version { version!: string; + guiVersion!: string; - constructor(version: string) { + constructor(version: string, guiVersion: string) { this.version = version; + this.guiVersion = guiVersion; } } diff --git a/webapp/wiremock/src/app/services/wiremock.service.ts b/webapp/wiremock/src/app/services/wiremock.service.ts index 1513cbe1ff..c1d92b3d3a 100644 --- a/webapp/wiremock/src/app/services/wiremock.service.ts +++ b/webapp/wiremock/src/app/services/wiremock.service.ts @@ -122,10 +122,14 @@ export class WiremockService { } getVersion(): Observable { - return this.defaultPipe(this.http.get(WiremockService.getUrl('version'), { - responseType: 'text' - }).pipe(map(v => { - return new Version(v); + const options = { + headers: { + "Accept": "application/json" + } + }; + return this.defaultPipe(this.http.get(WiremockService.getUrl('version'), options) + .pipe(map(v => { + return new Version(v.version, v.guiVersion); }))); } diff --git a/wiremock-jetty12/build.gradle b/wiremock-jetty12/build.gradle new file mode 100644 index 0000000000..780d1a36bb --- /dev/null +++ b/wiremock-jetty12/build.gradle @@ -0,0 +1,142 @@ +buildscript { + repositories { + maven { + url "https://oss.sonatype.org" + } + mavenCentral() + } +} + +plugins { + id 'java-library' + id 'java-test-fixtures' + id 'signing' + id 'maven-publish' + id 'com.diffplug.spotless' version '6.25.0' + id "org.sonarqube" version "4.4.1.3373" + id 'jacoco' + id 'idea' +} + +group 'org.wiremock' + +java { + sourceCompatibility = 17 + targetCompatibility = 17 + withSourcesJar() + withJavadocJar() +} + +project.ext { + versions = [ + jetty : '12.0.7', + junitJupiter : '5.10.2' + ] +} + +dependencies { + + api project(':'), { + exclude group: 'org.eclipse.jetty' + exclude group: 'org.eclipse.jetty.http2' + } + + api platform("org.eclipse.jetty:jetty-bom:$versions.jetty") + api platform("org.eclipse.jetty.ee10:jetty-ee10-bom:$versions.jetty") + api "org.eclipse.jetty:jetty-server" + api "org.eclipse.jetty:jetty-proxy" + api "org.eclipse.jetty:jetty-alpn-server" + api "org.eclipse.jetty:jetty-alpn-java-server" + api "org.eclipse.jetty:jetty-alpn-java-client" + api "org.eclipse.jetty:jetty-alpn-client" + api "org.eclipse.jetty.ee10:jetty-ee10-servlet" + api "org.eclipse.jetty.ee10:jetty-ee10-servlets" + api "org.eclipse.jetty.ee10:jetty-ee10-webapp" + api "org.eclipse.jetty.http2:jetty-http2-server:$versions.jetty" + api "org.eclipse.jetty:jetty-alpn-java-server:$versions.jetty" + + api "org.eclipse.jetty:jetty-rewrite:$versions.jetty" + api "org.eclipse.jetty.ee10.websocket:jetty-ee10-websocket-jakarta-server:$versions.jetty" + + testImplementation "org.eclipse.jetty.http2:jetty-http2-client:$versions.jetty" + testImplementation "org.eclipse.jetty.http2:jetty-http2-client-transport:$versions.jetty" + testImplementation "org.eclipse.jetty:jetty-alpn-java-client:$versions.jetty" + + testImplementation(testFixtures(project(":")), { + exclude group: 'org.eclipse.jetty' + exclude group: 'org.eclipse.jetty.http2' + }) +} + +idea { + module { + jdkName = '1.17' + } +} + +jar { + archiveBaseName.set('wiremock-jetty12') + manifest { + attributes("Implementation-Version": project.version) + attributes("Implementation-Title": "WireMock with Jetty 12") + } +} + +javadoc { + exclude "**/CertificateAuthority.java" + options.addStringOption('Xdoclint:none', '-quiet') +} + +signing { + required { + !version.toString().contains("SNAPSHOT") && (gradle.taskGraph.hasTask("uploadArchives") || gradle.taskGraph.hasTask("publish")) + } + def signingKey = providers.environmentVariable("OSSRH_GPG_SECRET_KEY").orElse("").get() + def signingPassphrase = providers.environmentVariable("OSSRH_GPG_SECRET_KEY_PASSWORD").orElse("").get() + if (!signingKey.isEmpty() && !signingPassphrase.isEmpty()) { + useInMemoryPgpKeys(signingKey, signingPassphrase) + } + sign publishing.publications +} + +if (JavaVersion.current().getMajorVersion() == '17') { + publishing { + repositories { + maven { + name = "GitHubPackages" + url = "https://maven.pkg.github.com/wiremock/wiremock" + credentials { + username = System.getenv("GITHUB_ACTOR") + password = System.getenv("GITHUB_TOKEN") + } + } + } + + publications { + mavenJava(MavenPublication) { publication -> + artifactId = "${jar.getArchiveBaseName().get()}" + from components.java + + pom.withXml { + asNode().appendNode('description', 'WireMock with Jetty 12 as its HTTP server') + asNode().children().last() + pomInfo + } + } + } + } +} + +task localRelease { + dependsOn clean, assemble, publishToMavenLocal +} + +test { + useJUnitPlatform() + + maxParallelForks = runningOnCI ? 1 : 3 + + testLogging { + events "FAILED", "SKIPPED" + exceptionFormat "full" + } +} diff --git a/wiremock-jetty12/src/main/java/com/github/tomakehurst/wiremock/jetty12/HttpProxyDetectingHandler.java b/wiremock-jetty12/src/main/java/com/github/tomakehurst/wiremock/jetty12/HttpProxyDetectingHandler.java new file mode 100644 index 0000000000..7502f3595d --- /dev/null +++ b/wiremock-jetty12/src/main/java/com/github/tomakehurst/wiremock/jetty12/HttpProxyDetectingHandler.java @@ -0,0 +1,51 @@ +/* + * Copyright (C) 2023-2024 Thomas Akehurst + * + * 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 + * + * http://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. + */ +package com.github.tomakehurst.wiremock.jetty12; + +import org.eclipse.jetty.server.Handler; +import org.eclipse.jetty.server.Request; +import org.eclipse.jetty.server.Response; +import org.eclipse.jetty.server.ServerConnector; +import org.eclipse.jetty.util.Callback; + +/** + * The Jetty 11 implementation was relying on relative request URI presence to detect the proxying + * request. Jetty 12 does not do that anymore and URI is always converted to absolute form. To keep + * proxy detection working, the Jetty 12 specific implementation does compare connector and URI + * ports (which are different in case of proxying request). + */ +public class HttpProxyDetectingHandler extends Handler.Abstract { + + public static final String IS_HTTP_PROXY_REQUEST_ATTRIBUTE = "wiremock.isHttpProxyRequest"; + + private final ServerConnector httpConnector; + + public HttpProxyDetectingHandler(ServerConnector httpConnector) { + this.httpConnector = httpConnector; + } + + @Override + public boolean handle(Request request, Response response, Callback callback) throws Exception { + final int httpPort = httpConnector.getLocalPort(); + + if (httpPort != request.getHttpURI().getPort() + && "http".equals(request.getHttpURI().getScheme())) { + request.setAttribute(IS_HTTP_PROXY_REQUEST_ATTRIBUTE, true); + } + + return false; + } +} diff --git a/wiremock-jetty12/src/main/java/com/github/tomakehurst/wiremock/jetty12/HttpsProxyDetectingHandler.java b/wiremock-jetty12/src/main/java/com/github/tomakehurst/wiremock/jetty12/HttpsProxyDetectingHandler.java new file mode 100644 index 0000000000..3383fe9bdc --- /dev/null +++ b/wiremock-jetty12/src/main/java/com/github/tomakehurst/wiremock/jetty12/HttpsProxyDetectingHandler.java @@ -0,0 +1,51 @@ +/* + * Copyright (C) 2023-2024 Thomas Akehurst + * + * 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 + * + * http://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. + */ +package com.github.tomakehurst.wiremock.jetty12; + +import java.net.InetSocketAddress; +import java.net.SocketAddress; +import org.eclipse.jetty.server.Handler; +import org.eclipse.jetty.server.Request; +import org.eclipse.jetty.server.Response; +import org.eclipse.jetty.server.ServerConnector; +import org.eclipse.jetty.util.Callback; + +public class HttpsProxyDetectingHandler extends Handler.Abstract { + + public static final String IS_HTTPS_PROXY_REQUEST_ATTRIBUTE = "wiremock.isHttpsProxyRequest"; + + private final ServerConnector mitmProxyConnector; + + public HttpsProxyDetectingHandler(ServerConnector mitmProxyConnector) { + this.mitmProxyConnector = mitmProxyConnector; + } + + @Override + public boolean handle(Request request, Response response, Callback callback) throws Exception { + final int httpsProxyPort = mitmProxyConnector.getLocalPort(); + + int localPort = -1; + SocketAddress local = request.getConnectionMetaData().getLocalSocketAddress(); + if (local instanceof InetSocketAddress) { + localPort = ((InetSocketAddress) local).getPort(); + } + + if (localPort == httpsProxyPort) { + request.setAttribute(IS_HTTPS_PROXY_REQUEST_ATTRIBUTE, true); + } + return false; + } +} diff --git a/wiremock-jetty12/src/main/java/com/github/tomakehurst/wiremock/jetty12/Jetty12HttpServer.java b/wiremock-jetty12/src/main/java/com/github/tomakehurst/wiremock/jetty12/Jetty12HttpServer.java new file mode 100644 index 0000000000..d0bee2b4ce --- /dev/null +++ b/wiremock-jetty12/src/main/java/com/github/tomakehurst/wiremock/jetty12/Jetty12HttpServer.java @@ -0,0 +1,468 @@ +/* + * Copyright (C) 2019-2024 Thomas Akehurst + * + * 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 + * + * http://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. + */ +package com.github.tomakehurst.wiremock.jetty12; + +import static com.github.tomakehurst.wiremock.common.Exceptions.throwUnchecked; +import static com.github.tomakehurst.wiremock.common.ResourceUtil.getResource; +import static com.github.tomakehurst.wiremock.core.WireMockApp.ADMIN_CONTEXT_ROOT; +import static com.github.tomakehurst.wiremock.jetty11.Jetty11Utils.createHttpConfig; +import static com.github.tomakehurst.wiremock.jetty11.SslContexts.buildManInTheMiddleSslContextFactory; +import static java.util.concurrent.Executors.newScheduledThreadPool; + +import com.github.tomakehurst.wiremock.common.AsynchronousResponseSettings; +import com.github.tomakehurst.wiremock.common.FileSource; +import com.github.tomakehurst.wiremock.common.HttpsSettings; +import com.github.tomakehurst.wiremock.common.JettySettings; +import com.github.tomakehurst.wiremock.common.Notifier; +import com.github.tomakehurst.wiremock.core.Options; +import com.github.tomakehurst.wiremock.core.WireMockApp; +import com.github.tomakehurst.wiremock.http.AdminRequestHandler; +import com.github.tomakehurst.wiremock.http.RequestHandler; +import com.github.tomakehurst.wiremock.http.StubRequestHandler; +import com.github.tomakehurst.wiremock.jetty.JettyFaultInjectorFactory; +import com.github.tomakehurst.wiremock.jetty.JettyHttpServer; +import com.github.tomakehurst.wiremock.jetty.JettyHttpUtils; +import com.github.tomakehurst.wiremock.jetty.websockets.WebSocketEndpoint; +import com.github.tomakehurst.wiremock.jetty11.Jetty11Utils; +import com.github.tomakehurst.wiremock.jetty11.SslContexts; +import com.github.tomakehurst.wiremock.servlet.ContentTypeSettingFilter; +import com.github.tomakehurst.wiremock.servlet.FaultInjectorFactory; +import com.github.tomakehurst.wiremock.servlet.MultipartRequestConfigurer; +import com.github.tomakehurst.wiremock.servlet.NotMatchedServlet; +import com.github.tomakehurst.wiremock.servlet.TrailingSlashFilter; +import com.github.tomakehurst.wiremock.servlet.WireMockHandlerDispatchingServlet; +import jakarta.servlet.DispatcherType; +import java.util.*; +import java.util.stream.Stream; +import org.eclipse.jetty.alpn.server.ALPNServerConnectionFactory; +import org.eclipse.jetty.ee10.servlet.DefaultServlet; +import org.eclipse.jetty.ee10.servlet.FilterHolder; +import org.eclipse.jetty.ee10.servlet.ServletContextHandler; +import org.eclipse.jetty.ee10.servlet.ServletContextRequest; +import org.eclipse.jetty.ee10.servlet.ServletHolder; +import org.eclipse.jetty.ee10.servlets.CrossOriginFilter; +import org.eclipse.jetty.ee10.websocket.jakarta.server.config.JakartaWebSocketServletContainerInitializer; +import org.eclipse.jetty.http.HttpVersion; +import org.eclipse.jetty.http.MimeTypes; +import org.eclipse.jetty.http2.server.HTTP2CServerConnectionFactory; +import org.eclipse.jetty.http2.server.HTTP2ServerConnectionFactory; +import org.eclipse.jetty.io.NetworkTrafficListener; +import org.eclipse.jetty.rewrite.handler.RewriteHandler; +import org.eclipse.jetty.rewrite.handler.RewriteRegexRule; +import org.eclipse.jetty.server.*; +import org.eclipse.jetty.server.handler.gzip.GzipHandler; +import org.eclipse.jetty.util.Callback; +import org.eclipse.jetty.util.ssl.SslContextFactory; + +public class Jetty12HttpServer extends JettyHttpServer { + + private ServerConnector mitmProxyConnector; + + public Jetty12HttpServer( + Options options, + AdminRequestHandler adminRequestHandler, + StubRequestHandler stubRequestHandler) { + super(options, adminRequestHandler, stubRequestHandler); + } + + @Override + protected ServerConnector createHttpConnector( + String bindAddress, int port, JettySettings jettySettings, NetworkTrafficListener listener) { + + HttpConfiguration httpConfig = createHttpConfig(jettySettings); + + ConnectionFactory[] connectionFactories = + Stream.of( + new HttpConnectionFactory(httpConfig), + options.getHttp2PlainDisabled() + ? null + : new HTTP2CServerConnectionFactory(httpConfig)) + .filter(Objects::nonNull) + .toArray(ConnectionFactory[]::new); + + return Jetty11Utils.createServerConnector( + jettyServer, bindAddress, jettySettings, port, listener, connectionFactories); + } + + @Override + protected ServerConnector createHttpsConnector( + String bindAddress, + HttpsSettings httpsSettings, + JettySettings jettySettings, + NetworkTrafficListener listener) { + + HttpConfiguration httpConfig = createHttpConfig(jettySettings); + + ConnectionFactory[] connectionFactories; + + if (!options.getHttp2TlsDisabled()) { + + SslContextFactory.Server http2SslContextFactory = + SslContexts.buildHttp2SslContextFactory(httpsSettings); + + HttpConnectionFactory http = new HttpConnectionFactory(httpConfig); + HTTP2ServerConnectionFactory h2 = new HTTP2ServerConnectionFactory(httpConfig); + + try { + ALPNServerConnectionFactory alpn = new ALPNServerConnectionFactory(); + + SslConnectionFactory ssl = + new SslConnectionFactory(http2SslContextFactory, alpn.getProtocol()); + + connectionFactories = new ConnectionFactory[] {ssl, alpn, h2, http}; + } catch (IllegalStateException e) { + SslConnectionFactory ssl = + new SslConnectionFactory(http2SslContextFactory, http.getProtocol()); + + connectionFactories = new ConnectionFactory[] {ssl, http}; + } + } else { + final SslContextFactory.Server sslContextFactory = + SslContexts.buildHttp1_1SslContextFactory(httpsSettings); + final SslConnectionFactory ssl = new SslConnectionFactory(sslContextFactory, "http/1.1"); + final HttpConnectionFactory http = new HttpConnectionFactory(httpConfig); + connectionFactories = new ConnectionFactory[] {ssl, http}; + } + + return Jetty11Utils.createServerConnector( + jettyServer, + bindAddress, + jettySettings, + httpsSettings.port(), + listener, + connectionFactories); + } + + @Override + protected void applyAdditionalServerConfiguration(Server jettyServer, Options options) { + if (options.browserProxySettings().enabled()) { + final SslConnectionFactory ssl = + new SslConnectionFactory( + buildManInTheMiddleSslContextFactory( + options.httpsSettings(), options.browserProxySettings(), options.notifier()), + /* + If the proxy CONNECT request is made over HTTPS, and the + actual content request is made using HTTP/2 tunneled over + HTTPS, and an exception is thrown, the server blocks for 30 + seconds before flushing the response. + + To fix this, force HTTP/1.1 over TLS when tunneling HTTPS. + + This also means the HTTP mitmProxyConnector does not need the alpn & + h2 connection factories as it will not use them. + + Unfortunately it has proven too hard to write a test to + demonstrate the bug; it requires an HTTP client capable of + doing ALPN & HTTP/2, which will only offer HTTP/1.1 in the + ALPN negotiation when using HTTPS for the initial CONNECT + request but will then offer both HTTP/1.1 and HTTP/2 for the + actual request (this is how curl 7.64.1 behaves!). Neither + Apache HTTP 4, 5, 5 Async, OkHttp, nor the Jetty client + could do this. It might be possible to write one using + Netty, but it would be hard and time consuming. + */ + HttpVersion.HTTP_1_1.asString()); + + JettySettings jettySettings = options.jettySettings(); + HttpConfiguration httpConfig = createHttpConfig(jettySettings); + HttpConnectionFactory http = new HttpConnectionFactory(httpConfig); + mitmProxyConnector = + new NetworkTrafficServerConnector(jettyServer, null, null, null, 2, 2, ssl, http); + + mitmProxyConnector.setPort(0); + mitmProxyConnector.setShutdownIdleTimeout( + jettySettings.getShutdownIdleTimeout().orElse(100L)); + + jettyServer.addConnector(mitmProxyConnector); + } + } + + @Override + protected Handler createHandler( + Options options, + AdminRequestHandler adminRequestHandler, + StubRequestHandler stubRequestHandler) { + Notifier notifier = options.notifier(); + ServletContextHandler adminContext = addAdminContext(adminRequestHandler, notifier); + ServletContextHandler mockServiceContext = + addMockServiceContext( + adminContext, + stubRequestHandler, + // Setting the files to the real path here since Jetty 12 does not include + // the servlet context path in forwarded request (and at the moment, there + // is not way to tell it to do so), the RequestDispatcher.FORWARD_SERVLET_PATH is + // ignored (only RequestDispatcher.INCLUDE_SERVLET_PATH is taken into account but + // that requires change from to RequestDispatcher#forward() to + // RequestDispatcher#include()). + options.filesRoot().child(WireMockApp.FILES_ROOT), + options.getAsynchronousResponseSettings(), + options.getChunkedEncodingPolicy(), + options.getStubCorsEnabled(), + options.browserProxySettings().enabled(), + notifier); + + final List handlers = new ArrayList<>(); + // wiremock-gui + // We prepend the rewrite handle for the wiremock gui web app to make sure that we route the + // requests properly + final RewriteHandler rewriteHandler = webAppRewriteContext(adminContext); + handlers.add(rewriteHandler); + + Handler.Abstract asyncTimeoutSettingHandler = + new Handler.Abstract() { + @Override + public boolean handle(Request request, Response response, Callback callback) { + if (request instanceof ServletContextRequest) { + final ServletContextRequest r = (ServletContextRequest) request; + r.getState().setTimeout(options.timeout()); + } + return false; + } + }; + handlers.addAll(Arrays.asList(extensionHandlers())); + handlers.add(adminContext); + handlers.add(asyncTimeoutSettingHandler); + + if (options.getGzipDisabled()) { + handlers.add(mockServiceContext); + } else { + addGZipHandler(mockServiceContext, handlers); + } + + if (options.browserProxySettings().enabled()) { + handlers.add(0, new HttpProxyDetectingHandler(httpConnector)); + handlers.add(0, new HttpsProxyDetectingHandler(mitmProxyConnector)); + handlers.add(0, new ManInTheMiddleSslConnectHandler(mitmProxyConnector)); + } + + return new Handler.Sequence(handlers); + } + + protected void decorateAdminServiceContextBeforeConfig( + ServletContextHandler adminServiceContext) {} + + protected void decorateAdminServiceContextAfterConfig( + ServletContextHandler adminServiceContext) {} + + // wiremock-gui + /** + * Rewrite web app. We use Angular. We must rewrite every /__admin/webapp path to the index.html + * of our single page application. + * + * @param adminContextHandler admin context handler is used to provide the static file content + * @return the rewrite handler + */ + private RewriteHandler webAppRewriteContext(ServletContextHandler adminContextHandler) { + final RewriteHandler rewrite = new RewriteHandler(); + // rewrite.setRewriteRequestURI(true); + // rewrite.setRewritePathInfo(true); + + RewriteRegexRule rewriteRule = new RewriteRegexRule(); + rewriteRule.setRegex("/__admin/webapp/(mappings|matched|unmatched|state|files).*"); + rewriteRule.setReplacement("/__admin/webapp/index.html"); + rewrite.addRule(rewriteRule); + + rewrite.setHandler(adminContextHandler); + + return rewrite; + } + + private void addCorsFilter(ServletContextHandler context) { + context.addFilter(buildCorsFilter(), "/*", EnumSet.of(DispatcherType.REQUEST)); + } + + private ServletContextHandler addAdminContext( + AdminRequestHandler adminRequestHandler, Notifier notifier) { + ServletContextHandler adminContext = new ServletContextHandler(); + adminContext.setServer(jettyServer); + adminContext.setContextPath(ADMIN_CONTEXT_ROOT); + + decorateAdminServiceContextBeforeConfig(adminContext); + + adminContext.setInitParameter("org.eclipse.jetty.servlet.Default.maxCacheSize", "0"); + + String javaVendor = System.getProperty("java.vendor"); + if (javaVendor != null && javaVendor.toLowerCase().contains("android")) { + // Special case for Android, fixes IllegalArgumentException("resource assets not found."): + // The Android ClassLoader apparently does not resolve directories. + // Furthermore, lib assets will be merged into a single asset directory when a jar file is + // assimilated into an apk. + // As resources can be addressed like "assets/swagger-ui/index.html", a static path element + // will suffice. + adminContext.setInitParameter("org.eclipse.jetty.servlet.Default.resourceBase", "assets"); + } else { + adminContext.setInitParameter( + "org.eclipse.jetty.servlet.Default.resourceBase", + getResource(JettyHttpServer.class, "assets").toString()); + } + + getResource(JettyHttpServer.class, "assets/swagger-ui/index.html"); + // Jetty 12 changed the way welcome files are being served (the context path is not being + // taken into account anymore). It is possible to somewhat change this behavior by altering + // welcome mode of ServletResourceService but this parameter is only partially supported by + // DefaultServlet (not all modes) and in general needs servlet subclassing. Taking an easier + // approach here with extended welcome files list. + adminContext.setWelcomeFiles(new String[] {"index.html", "index.jsp", "swagger-ui/index.html"}); + + adminContext.setInitParameter("org.eclipse.jetty.servlet.Default.dirAllowed", "false"); + ServletHolder swaggerUiServletHolder = + adminContext.addServlet(DefaultServlet.class, "/swagger-ui/*"); + swaggerUiServletHolder.setAsyncSupported(false); + adminContext.addServlet(DefaultServlet.class, "/recorder/*"); + + ServletHolder servletHolder = + adminContext.addServlet(WireMockHandlerDispatchingServlet.class, "/"); + servletHolder.setInitParameter( + RequestHandler.HANDLER_CLASS_KEY, AdminRequestHandler.class.getName()); + adminContext.setAttribute(AdminRequestHandler.class.getName(), adminRequestHandler); + adminContext.setAttribute(Notifier.KEY, notifier); + + adminContext.setAttribute(MultipartRequestConfigurer.KEY, buildMultipartRequestConfigurer()); + + adminContext.addServlet(NotMatchedServlet.class, "/not-matched"); + + // wiremock-gui Include into admin context + ServletHolder webapp = adminContext.addServlet(DefaultServlet.class, "/webapp/*"); + webapp.setAsyncSupported(false); + + // wiremock-gui Include websocket into admin context + JakartaWebSocketServletContainerInitializer.configure( + adminContext, + (servletContext, serverContainer) -> { + serverContainer.addEndpoint(WebSocketEndpoint.class); + }); + + addCorsFilter(adminContext); + + decorateAdminServiceContextAfterConfig(adminContext); + + return adminContext; + } + + private ServletContextHandler addMockServiceContext( + ServletContextHandler adminContext, + StubRequestHandler stubRequestHandler, + FileSource fileSource, + AsynchronousResponseSettings asynchronousResponseSettings, + Options.ChunkedEncodingPolicy chunkedEncodingPolicy, + boolean stubCorsEnabled, + boolean browserProxyingEnabled, + Notifier notifier) { + ServletContextHandler mockServiceContext = new ServletContextHandler(); + mockServiceContext.setServer(jettyServer); + mockServiceContext.setContextPath("/"); + + decorateMockServiceContextBeforeConfig(mockServiceContext); + + mockServiceContext.setInitParameter("org.eclipse.jetty.servlet.Default.maxCacheSize", "0"); + mockServiceContext.setInitParameter( + "org.eclipse.jetty.servlet.Default.resourceBase", fileSource.getPath()); + mockServiceContext.setInitParameter("org.eclipse.jetty.servlet.Default.dirAllowed", "false"); + + mockServiceContext.addServlet(DefaultServlet.class, FILES_URL_MATCH); + + final Jetty12HttpUtils utils = new Jetty12HttpUtils(); + mockServiceContext.setAttribute(JettyHttpUtils.class.getName(), utils); + + mockServiceContext.setAttribute( + JettyFaultInjectorFactory.class.getName(), new JettyFaultInjectorFactory(utils)); + mockServiceContext.setAttribute(StubRequestHandler.class.getName(), stubRequestHandler); + mockServiceContext.setAttribute(Notifier.KEY, notifier); + mockServiceContext.setAttribute( + Options.ChunkedEncodingPolicy.class.getName(), chunkedEncodingPolicy); + mockServiceContext.setAttribute("browserProxyingEnabled", browserProxyingEnabled); + ServletHolder servletHolder = + mockServiceContext.addServlet(WireMockHandlerDispatchingServlet.class, "/"); + servletHolder.setInitOrder(1); + servletHolder.setInitParameter( + RequestHandler.HANDLER_CLASS_KEY, StubRequestHandler.class.getName()); + servletHolder.setInitParameter( + FaultInjectorFactory.INJECTOR_CLASS_KEY, JettyFaultInjectorFactory.class.getName()); + servletHolder.setInitParameter( + WireMockHandlerDispatchingServlet.SHOULD_FORWARD_TO_FILES_CONTEXT, "true"); + + if (asynchronousResponseSettings.isEnabled()) { + scheduledExecutorService = newScheduledThreadPool(asynchronousResponseSettings.getThreads()); + mockServiceContext.setAttribute( + WireMockHandlerDispatchingServlet.ASYNCHRONOUS_RESPONSE_EXECUTOR, + scheduledExecutorService); + } + + mockServiceContext.setAttribute( + MultipartRequestConfigurer.KEY, buildMultipartRequestConfigurer()); + + MimeTypes.Mutable mimeTypes = mockServiceContext.getMimeTypes(); + // For files without extension, use "application/json" as the default in case + // file extension is not provided(and content type could not be detected). + mimeTypes.addMimeMapping("*", "application/json"); + mimeTypes.addMimeMapping("json", "application/json"); + mimeTypes.addMimeMapping("html", "text/html"); + mimeTypes.addMimeMapping("xml", "application/xml"); + mimeTypes.addMimeMapping("txt", "text/plain"); + + mockServiceContext.setWelcomeFiles( + new String[] {"index.json", "index.html", "index.xml", "index.txt"}); + + // Jetty 12 does not currently support cross context dispatch, we + // need to use the adminContext explicitly. + NotFoundHandler errorHandler = new NotFoundHandler(adminContext); + mockServiceContext.setErrorHandler(errorHandler); + + mockServiceContext.addFilter( + ContentTypeSettingFilter.class, FILES_URL_MATCH, EnumSet.of(DispatcherType.FORWARD)); + mockServiceContext.addFilter( + TrailingSlashFilter.class, FILES_URL_MATCH, EnumSet.allOf(DispatcherType.class)); + + if (stubCorsEnabled) { + addCorsFilter(mockServiceContext); + } + + decorateMockServiceContextAfterConfig(mockServiceContext); + + return mockServiceContext; + } + + protected void decorateMockServiceContextBeforeConfig(ServletContextHandler mockServiceContext) {} + + protected void decorateMockServiceContextAfterConfig(ServletContextHandler mockServiceContext) {} + + private void addGZipHandler(ServletContextHandler mockServiceContext, List handlers) { + try { + GzipHandler gzipHandler = new GzipHandler(); + gzipHandler.addIncludedMethods(GZIPPABLE_METHODS); + gzipHandler.setHandler(mockServiceContext); + gzipHandler.setVary(null); + handlers.add(gzipHandler); + } catch (Exception e) { + throwUnchecked(e); + } + } + + private FilterHolder buildCorsFilter() { + FilterHolder filterHolder = new FilterHolder(CrossOriginFilter.class); + filterHolder.setInitParameters( + Map.of( + "chainPreflight", + "false", + "allowedOrigins", + "*", + "allowedHeaders", + "*", + "allowedMethods", + "OPTIONS,GET,POST,PUT,PATCH,DELETE")); + return filterHolder; + } +} diff --git a/wiremock-jetty12/src/main/java/com/github/tomakehurst/wiremock/jetty12/Jetty12HttpServerFactory.java b/wiremock-jetty12/src/main/java/com/github/tomakehurst/wiremock/jetty12/Jetty12HttpServerFactory.java new file mode 100644 index 0000000000..4cea5ee60f --- /dev/null +++ b/wiremock-jetty12/src/main/java/com/github/tomakehurst/wiremock/jetty12/Jetty12HttpServerFactory.java @@ -0,0 +1,38 @@ +/* + * Copyright (C) 2024 Thomas Akehurst + * + * 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 + * + * http://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. + */ +package com.github.tomakehurst.wiremock.jetty12; + +import com.github.tomakehurst.wiremock.core.Options; +import com.github.tomakehurst.wiremock.http.AdminRequestHandler; +import com.github.tomakehurst.wiremock.http.HttpServer; +import com.github.tomakehurst.wiremock.http.HttpServerFactory; +import com.github.tomakehurst.wiremock.http.StubRequestHandler; + +public class Jetty12HttpServerFactory implements HttpServerFactory { + + @Override + public String getName() { + return "jetty12-http-server-factory"; + } + + @Override + public HttpServer buildHttpServer( + Options options, + AdminRequestHandler adminRequestHandler, + StubRequestHandler stubRequestHandler) { + return new Jetty12HttpServer(options, adminRequestHandler, stubRequestHandler); + } +} diff --git a/wiremock-jetty12/src/main/java/com/github/tomakehurst/wiremock/jetty12/Jetty12HttpUtils.java b/wiremock-jetty12/src/main/java/com/github/tomakehurst/wiremock/jetty12/Jetty12HttpUtils.java new file mode 100644 index 0000000000..7ee08411b6 --- /dev/null +++ b/wiremock-jetty12/src/main/java/com/github/tomakehurst/wiremock/jetty12/Jetty12HttpUtils.java @@ -0,0 +1,89 @@ +/* + * Copyright (C) 2015-2024 Thomas Akehurst + * + * 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 + * + * http://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. + */ +package com.github.tomakehurst.wiremock.jetty12; + +import static com.github.tomakehurst.wiremock.jetty12.HttpProxyDetectingHandler.IS_HTTP_PROXY_REQUEST_ATTRIBUTE; +import static com.github.tomakehurst.wiremock.jetty12.HttpsProxyDetectingHandler.IS_HTTPS_PROXY_REQUEST_ATTRIBUTE; + +import com.github.tomakehurst.wiremock.jetty.JettyHttpUtils; +import jakarta.servlet.ServletResponse; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import jakarta.servlet.http.HttpServletResponseWrapper; +import java.net.Socket; +import java.nio.channels.SocketChannel; +import org.eclipse.jetty.ee10.servlet.ServletApiResponse; +import org.eclipse.jetty.io.EndPoint; +import org.eclipse.jetty.io.SelectableChannelEndPoint; +import org.eclipse.jetty.io.ssl.SslConnection; +import org.eclipse.jetty.server.AbstractMetaDataConnection; +import org.eclipse.jetty.server.Response; + +public class Jetty12HttpUtils implements JettyHttpUtils { + @Override + public Response unwrapResponse(HttpServletResponse httpServletResponse) { + if (httpServletResponse instanceof HttpServletResponseWrapper) { + ServletResponse unwrapped = ((HttpServletResponseWrapper) httpServletResponse).getResponse(); + return (Response) unwrapped; + } else if (httpServletResponse instanceof ServletApiResponse) { + Response unwrapped = ((ServletApiResponse) httpServletResponse).getResponse(); + return unwrapped; + } + + return (Response) httpServletResponse; + } + + @Override + public Socket socket(Response response) { + final AbstractMetaDataConnection connectionMetaData = + (AbstractMetaDataConnection) response.getRequest().getConnectionMetaData(); + SelectableChannelEndPoint ep = (SelectableChannelEndPoint) connectionMetaData.getEndPoint(); + return ((SocketChannel) ep.getChannel()).socket(); + } + + @Override + public Socket tlsSocket(Response response) { + final AbstractMetaDataConnection connectionMetaData = + (AbstractMetaDataConnection) response.getRequest().getConnectionMetaData(); + final SslConnection.SslEndPoint sslEndpoint = + (SslConnection.SslEndPoint) connectionMetaData.getEndPoint(); + final SelectableChannelEndPoint endpoint = + (SelectableChannelEndPoint) sslEndpoint.getSslConnection().getEndPoint(); + return ((SocketChannel) endpoint.getChannel()).socket(); + } + + @Override + public void setStatusWithReason( + int status, String reason, HttpServletResponse httpServletResponse) { + // Servlet 6 is not accepting the reason / message anymore, consequently Jetty 12 + // completely eliminated the possibility to pass reason / message along with a status + // in case of HTTP 1.x communication. + httpServletResponse.setStatus(status); + } + + @Override + public EndPoint unwrapEndPoint(Response jettyResponse) { + final AbstractMetaDataConnection connectionMetaData = + (AbstractMetaDataConnection) jettyResponse.getRequest().getConnectionMetaData(); + return connectionMetaData.getEndPoint(); + } + + @Override + public boolean isBrowserProxyRequest(HttpServletRequest request) { + return Boolean.TRUE.equals(request.getAttribute(IS_HTTPS_PROXY_REQUEST_ATTRIBUTE)) + || Boolean.TRUE.equals(request.getAttribute(IS_HTTP_PROXY_REQUEST_ATTRIBUTE)); + } +} diff --git a/wiremock-jetty12/src/main/java/com/github/tomakehurst/wiremock/jetty12/ManInTheMiddleSslConnectHandler.java b/wiremock-jetty12/src/main/java/com/github/tomakehurst/wiremock/jetty12/ManInTheMiddleSslConnectHandler.java new file mode 100644 index 0000000000..0e3816011f --- /dev/null +++ b/wiremock-jetty12/src/main/java/com/github/tomakehurst/wiremock/jetty12/ManInTheMiddleSslConnectHandler.java @@ -0,0 +1,63 @@ +/* + * Copyright (C) 2020-2024 Thomas Akehurst + * + * 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 + * + * http://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. + */ +package com.github.tomakehurst.wiremock.jetty12; + +import static com.github.tomakehurst.wiremock.common.ParameterUtils.getFirstNonNull; + +import java.io.Closeable; +import java.net.InetSocketAddress; +import java.nio.channels.SocketChannel; +import org.eclipse.jetty.server.*; +import org.eclipse.jetty.server.handler.ConnectHandler; +import org.eclipse.jetty.util.Promise; + +public class ManInTheMiddleSslConnectHandler extends ConnectHandler { + + private final ServerConnector mitmProxyConnector; + + public ManInTheMiddleSslConnectHandler(ServerConnector mitmProxyConnector) { + this.mitmProxyConnector = mitmProxyConnector; + } + + @Override + protected void connectToServer( + Request request, String ignoredHost, int ignoredPort, Promise promise) { + SocketChannel channel = null; + try { + channel = SocketChannel.open(); + channel.socket().setTcpNoDelay(true); + channel.configureBlocking(false); + + String host = getFirstNonNull(mitmProxyConnector.getHost(), "localhost"); + int port = mitmProxyConnector.getLocalPort(); + InetSocketAddress address = newConnectAddress(host, port); + + channel.connect(address); + promise.succeeded(channel); + } catch (Throwable x) { + close(channel); + promise.failed(x); + } + } + + private void close(Closeable closeable) { + try { + if (closeable != null) closeable.close(); + } catch (Throwable x) { + /* Ignore */ + } + } +} diff --git a/wiremock-jetty12/src/main/java/com/github/tomakehurst/wiremock/jetty12/NotFoundHandler.java b/wiremock-jetty12/src/main/java/com/github/tomakehurst/wiremock/jetty12/NotFoundHandler.java new file mode 100644 index 0000000000..5137d43565 --- /dev/null +++ b/wiremock-jetty12/src/main/java/com/github/tomakehurst/wiremock/jetty12/NotFoundHandler.java @@ -0,0 +1,70 @@ +/* + * Copyright (C) 2017-2024 Thomas Akehurst + * + * 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 + * + * http://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. + */ +package com.github.tomakehurst.wiremock.jetty12; + +import jakarta.servlet.ServletException; +import org.eclipse.jetty.ee10.servlet.Dispatcher; +import org.eclipse.jetty.ee10.servlet.ServletContextHandler; +import org.eclipse.jetty.ee10.servlet.ServletContextRequest; +import org.eclipse.jetty.ee10.servlet.ServletContextResponse; +import org.eclipse.jetty.server.Request; +import org.eclipse.jetty.server.Response; +import org.eclipse.jetty.server.handler.ErrorHandler; +import org.eclipse.jetty.util.Callback; + +public class NotFoundHandler extends ErrorHandler { + + private final ErrorHandler DEFAULT_HANDLER = new ErrorHandler(); + + private final ServletContextHandler adminServiceHandler; + + public NotFoundHandler(ServletContextHandler adminServiceHandler) { + this.adminServiceHandler = adminServiceHandler; + } + + @Override + public boolean errorPageForMethod(String method) { + return true; + } + + @Override + public boolean handle(Request request, Response response, Callback callback) throws Exception { + if (response.getStatus() == 404) { + + // Jetty 12 does not currently support cross context dispatch + Dispatcher requestDispatcher = + (Dispatcher) adminServiceHandler.getServletContext().getRequestDispatcher("/not-matched"); + + try { + requestDispatcher.error( + ((ServletContextRequest) request).getServletApiRequest(), + ((ServletContextResponse) response).getServletApiResponse()); + callback.succeeded(); + return true; + } catch (ServletException e) { + callback.failed(e); + } + } else { + try { + return DEFAULT_HANDLER.handle(request, response, callback); + } catch (Exception e) { + callback.failed(e); + } + } + + return false; + } +} diff --git a/wiremock-jetty12/src/main/resources/META-INF/services/com.github.tomakehurst.wiremock.extension.Extension b/wiremock-jetty12/src/main/resources/META-INF/services/com.github.tomakehurst.wiremock.extension.Extension new file mode 100644 index 0000000000..fe855aafef --- /dev/null +++ b/wiremock-jetty12/src/main/resources/META-INF/services/com.github.tomakehurst.wiremock.extension.Extension @@ -0,0 +1 @@ +com.github.tomakehurst.wiremock.jetty12.Jetty12HttpServerFactory \ No newline at end of file diff --git a/wiremock-jetty12/src/test/java/com/github/tomakehurst/wiremock/jetty12/FaultsTest.java b/wiremock-jetty12/src/test/java/com/github/tomakehurst/wiremock/jetty12/FaultsTest.java new file mode 100644 index 0000000000..d8d418e368 --- /dev/null +++ b/wiremock-jetty12/src/test/java/com/github/tomakehurst/wiremock/jetty12/FaultsTest.java @@ -0,0 +1,101 @@ +/* + * Copyright (C) 2024 Thomas Akehurst + * + * 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 + * + * http://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. + */ +package com.github.tomakehurst.wiremock.jetty12; + +import static com.github.tomakehurst.wiremock.client.WireMock.*; +import static com.github.tomakehurst.wiremock.core.WireMockConfiguration.wireMockConfig; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.instanceOf; +import static org.hamcrest.Matchers.is; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import com.github.tomakehurst.wiremock.client.WireMock; +import com.github.tomakehurst.wiremock.http.Fault; +import com.github.tomakehurst.wiremock.junit5.WireMockExtension; +import com.github.tomakehurst.wiremock.testsupport.WireMockResponse; +import com.github.tomakehurst.wiremock.testsupport.WireMockTestClient; +import org.apache.hc.core5.http.MalformedChunkCodingException; +import org.apache.hc.core5.http.NoHttpResponseException; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +public class FaultsTest { + + @RegisterExtension + static WireMockExtension wm = + WireMockExtension.newInstance().options(wireMockConfig().dynamicPort()).build(); + + static WireMockTestClient testClient; + + @BeforeAll + static void init() { + testClient = new WireMockTestClient(wm.getPort()); + WireMock.configureFor(wm.getPort()); + } + + @Test + public void connectionResetByPeerFault() { + stubFor( + get(urlEqualTo("/connection/reset")) + .willReturn(aResponse().withFault(Fault.CONNECTION_RESET_BY_PEER))); + + RuntimeException runtimeException = + assertThrows(RuntimeException.class, () -> testClient.get("/connection/reset")); + assertThat(runtimeException.getMessage(), is("java.net.SocketException: Connection reset")); + } + + @Test + public void emptyResponseFault() { + stubFor( + get(urlEqualTo("/empty/response")).willReturn(aResponse().withFault(Fault.EMPTY_RESPONSE))); + + getAndAssertUnderlyingExceptionInstanceClass("/empty/response", NoHttpResponseException.class); + } + + @Test + public void malformedResponseChunkFault() { + stubFor( + get(urlEqualTo("/malformed/response")) + .willReturn(aResponse().withFault(Fault.MALFORMED_RESPONSE_CHUNK))); + + getAndAssertUnderlyingExceptionInstanceClass( + "/malformed/response", MalformedChunkCodingException.class); + } + + @Test + public void randomDataOnSocketFault() { + stubFor( + get(urlEqualTo("/random/data")) + .willReturn(aResponse().withFault(Fault.RANDOM_DATA_THEN_CLOSE))); + + getAndAssertUnderlyingExceptionInstanceClass("/random/data", NoHttpResponseException.class); + } + + private void getAndAssertUnderlyingExceptionInstanceClass(String url, Class expectedClass) { + boolean thrown = false; + try { + WireMockResponse response = testClient.get(url); + response.content(); + } catch (Exception e) { + assertThat(e.getCause(), instanceOf(expectedClass)); + thrown = true; + } + + assertTrue(thrown, "No exception was thrown"); + } +} diff --git a/wiremock-jetty12/src/test/java/com/github/tomakehurst/wiremock/jetty12/Http2AcceptanceTest.java b/wiremock-jetty12/src/test/java/com/github/tomakehurst/wiremock/jetty12/Http2AcceptanceTest.java new file mode 100644 index 0000000000..25d22f08b8 --- /dev/null +++ b/wiremock-jetty12/src/test/java/com/github/tomakehurst/wiremock/jetty12/Http2AcceptanceTest.java @@ -0,0 +1,80 @@ +/* + * Copyright (C) 2019-2024 Thomas Akehurst + * + * 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 + * + * http://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. + */ +package com.github.tomakehurst.wiremock.jetty12; + +import static com.github.tomakehurst.wiremock.client.WireMock.get; +import static com.github.tomakehurst.wiremock.client.WireMock.ok; +import static com.github.tomakehurst.wiremock.core.WireMockConfiguration.wireMockConfig; +import static org.eclipse.jetty.http.HttpVersion.HTTP_2; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.is; + +import com.github.tomakehurst.wiremock.http.HttpClientFactory; +import com.github.tomakehurst.wiremock.junit5.WireMockExtension; +import org.apache.hc.client5.http.classic.methods.HttpGet; +import org.apache.hc.client5.http.impl.classic.CloseableHttpClient; +import org.apache.hc.client5.http.impl.classic.CloseableHttpResponse; +import org.eclipse.jetty.client.ContentResponse; +import org.eclipse.jetty.client.HttpClient; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +public class Http2AcceptanceTest { + + @RegisterExtension + public WireMockExtension wm = + WireMockExtension.newInstance() + .options( + wireMockConfig() + .extensionScanningEnabled(true) + .dynamicPort() + .dynamicHttpsPort() + .httpServerFactory(new Jetty12HttpServerFactory())) + .build(); + + @Test + public void supportsHttp2Connections() throws Exception { + HttpClient client = Http2ClientFactory.create(); + + wm.stubFor(get("/thing").willReturn(ok("HTTP/2 response"))); + + ContentResponse response = client.GET(wm.getRuntimeInfo().getHttpsBaseUrl() + "/thing"); + assertThat(response.getStatus(), is(200)); + } + + @Test + public void supportsHttp2PlaintextConnections() throws Exception { + HttpClient client = Http2ClientFactory.create(); + + wm.stubFor(get("/thing").willReturn(ok("HTTP/2 response"))); + + ContentResponse response = client.GET(wm.getRuntimeInfo().getHttpBaseUrl() + "/thing"); + assertThat(response.getVersion(), is(HTTP_2)); + assertThat(response.getStatus(), is(200)); + } + + @Test + public void supportsHttp1_1Connections() throws Exception { + CloseableHttpClient client = HttpClientFactory.createClient(); + + wm.stubFor(get("/thing").willReturn(ok("HTTP/1.1 response"))); + + HttpGet get = new HttpGet(wm.getRuntimeInfo().getHttpsBaseUrl() + "/thing"); + try (CloseableHttpResponse response = client.execute(get)) { + assertThat(response.getCode(), is(200)); + } + } +} diff --git a/wiremock-jetty12/src/test/java/com/github/tomakehurst/wiremock/jetty12/Http2ClientFactory.java b/wiremock-jetty12/src/test/java/com/github/tomakehurst/wiremock/jetty12/Http2ClientFactory.java new file mode 100644 index 0000000000..1c8611279c --- /dev/null +++ b/wiremock-jetty12/src/test/java/com/github/tomakehurst/wiremock/jetty12/Http2ClientFactory.java @@ -0,0 +1,43 @@ +/* + * Copyright (C) 2019-2024 Thomas Akehurst + * + * 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 + * + * http://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. + */ +package com.github.tomakehurst.wiremock.jetty12; + +import com.github.tomakehurst.wiremock.common.Exceptions; +import org.eclipse.jetty.client.HttpClient; +import org.eclipse.jetty.client.HttpClientTransport; +import org.eclipse.jetty.http2.client.HTTP2Client; +import org.eclipse.jetty.http2.client.transport.HttpClientTransportOverHTTP2; +import org.eclipse.jetty.io.ClientConnector; +import org.eclipse.jetty.util.ssl.SslContextFactory; + +class Http2ClientFactory { + public static HttpClient create() { + final SslContextFactory.Client sslContextFactory = new SslContextFactory.Client(true); + final ClientConnector connector = new ClientConnector(); + connector.setSslContextFactory(sslContextFactory); + HttpClientTransport transport = new HttpClientTransportOverHTTP2(new HTTP2Client(connector)); + HttpClient httpClient = new HttpClient(transport); + + httpClient.setFollowRedirects(false); + try { + httpClient.start(); + } catch (Exception e) { + return Exceptions.throwUnchecked(e, HttpClient.class); + } + + return httpClient; + } +} diff --git a/wiremock-jetty12/src/test/java/com/github/tomakehurst/wiremock/jetty12/Http2DisabledAcceptanceTest.java b/wiremock-jetty12/src/test/java/com/github/tomakehurst/wiremock/jetty12/Http2DisabledAcceptanceTest.java new file mode 100644 index 0000000000..9b8f6c6081 --- /dev/null +++ b/wiremock-jetty12/src/test/java/com/github/tomakehurst/wiremock/jetty12/Http2DisabledAcceptanceTest.java @@ -0,0 +1,119 @@ +/* + * Copyright (C) 2024 Thomas Akehurst + * + * 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 + * + * http://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. + */ +package com.github.tomakehurst.wiremock.jetty12; + +import static com.github.tomakehurst.wiremock.client.WireMock.get; +import static com.github.tomakehurst.wiremock.client.WireMock.ok; +import static com.github.tomakehurst.wiremock.core.WireMockConfiguration.wireMockConfig; +import static java.net.http.HttpClient.Version.HTTP_1_1; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.is; + +import com.github.tomakehurst.wiremock.junit5.WireMockExtension; +import java.net.Socket; +import java.net.URI; +import java.net.http.HttpClient; +import java.net.http.HttpRequest; +import java.net.http.HttpResponse; +import java.security.SecureRandom; +import java.security.cert.X509Certificate; +import javax.net.ssl.SSLContext; +import javax.net.ssl.SSLEngine; +import javax.net.ssl.TrustManager; +import javax.net.ssl.X509ExtendedTrustManager; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +public class Http2DisabledAcceptanceTest { + + @RegisterExtension + public WireMockExtension wm = + WireMockExtension.newInstance() + .options( + wireMockConfig() + .dynamicPort() + .dynamicHttpsPort() + .http2PlainDisabled(true) + .http2TlsDisabled(true) + .httpServerFactory(new Jetty12HttpServerFactory())) + .build(); + + HttpClient client; + + @BeforeEach + void init() throws Exception { + client = HttpClient.newBuilder().sslContext(trustEverything()).build(); + } + + @Test + public void usesHttp1_1OverPlainText() throws Exception { + wm.stubFor(get("/thing").willReturn(ok("HTTP/2 response"))); + + URI uri = URI.create(wm.getRuntimeInfo().getHttpBaseUrl() + "/thing"); + + HttpResponse response = + client.send(HttpRequest.newBuilder(uri).build(), HttpResponse.BodyHandlers.ofString()); + assertThat(response.version(), is(HTTP_1_1)); + assertThat(response.statusCode(), is(200)); + } + + @Test + void usesHttp1_1OverTls() throws Exception { + wm.stubFor(get("/thing").willReturn(ok("HTTP/2 response"))); + + URI uri = URI.create(wm.getRuntimeInfo().getHttpsBaseUrl() + "/thing"); + + HttpResponse response = + client.send(HttpRequest.newBuilder(uri).build(), HttpResponse.BodyHandlers.ofString()); + assertThat(response.version(), is(HTTP_1_1)); + assertThat(response.statusCode(), is(200)); + } + + private SSLContext trustEverything() throws Exception { + X509ExtendedTrustManager trustManager = + new X509ExtendedTrustManager() { + @Override + public X509Certificate[] getAcceptedIssuers() { + return new X509Certificate[] {}; + } + + @Override + public void checkClientTrusted(X509Certificate[] chain, String authType) {} + + @Override + public void checkServerTrusted(X509Certificate[] chain, String authType) {} + + @Override + public void checkClientTrusted(X509Certificate[] chain, String authType, Socket socket) {} + + @Override + public void checkServerTrusted(X509Certificate[] chain, String authType, Socket socket) {} + + @Override + public void checkClientTrusted( + X509Certificate[] chain, String authType, SSLEngine engine) {} + + @Override + public void checkServerTrusted( + X509Certificate[] chain, String authType, SSLEngine engine) {} + }; + SSLContext sslContext = SSLContext.getInstance("TLS"); + sslContext.init(null, new TrustManager[] {trustManager}, new SecureRandom()); + + return sslContext; + } +} diff --git a/wiremock-jetty12/src/test/java/com/github/tomakehurst/wiremock/jetty12/Jetty12MultipartParser.java b/wiremock-jetty12/src/test/java/com/github/tomakehurst/wiremock/jetty12/Jetty12MultipartParser.java new file mode 100644 index 0000000000..2ebdc888f4 --- /dev/null +++ b/wiremock-jetty12/src/test/java/com/github/tomakehurst/wiremock/jetty12/Jetty12MultipartParser.java @@ -0,0 +1,68 @@ +/* + * Copyright (C) 2018-2024 Thomas Akehurst + * + * 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 + * + * http://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. + */ +package com.github.tomakehurst.wiremock.jetty12; + +import static com.github.tomakehurst.wiremock.common.Exceptions.throwUnchecked; + +import com.github.tomakehurst.wiremock.http.Request; +import com.github.tomakehurst.wiremock.servlet.WireMockHttpServletMultipartAdapter; +import jakarta.servlet.http.Part; +import java.io.File; +import java.util.Collection; +import java.util.concurrent.CompletableFuture; +import java.util.stream.Collectors; +import org.eclipse.jetty.client.BytesRequestContent; +import org.eclipse.jetty.ee10.servlet.ServletMultiPartFormData.Parts; +import org.eclipse.jetty.http.MultiPart; +import org.eclipse.jetty.http.MultiPartFormData; +import org.eclipse.jetty.io.Content; +import org.eclipse.jetty.util.Attributes; + +public class Jetty12MultipartParser + implements com.github.tomakehurst.wiremock.MultipartParserLoader.MultipartParser { + @SuppressWarnings("unchecked") + @Override + public Collection parse(byte[] body, String contentType) { + String boundary = MultiPart.extractBoundary(contentType); + + final File filesDirectory = new File(System.getProperty("java.io.tmpdir")); + final CompletableFuture> parts = + MultiPartFormData.from( + Attributes.NULL, + boundary, + parser -> { + try { + // No existing core parts, so we need to configure the parser. + + Content.Source source = new BytesRequestContent(body); + parser.setFilesDirectory(filesDirectory.toPath()); + return parser.parse(source); + } catch (Throwable failure) { + return CompletableFuture.failedFuture(failure); + } + }) + .thenApply( + formDataParts -> new Parts(filesDirectory.toPath(), formDataParts).getParts()); + + try { + return parts.get().stream() + .map(WireMockHttpServletMultipartAdapter::from) + .collect(Collectors.toList()); + } catch (Exception e) { + return throwUnchecked(e, Collection.class); + } + } +} diff --git a/wiremock-jetty12/src/test/java/com/github/tomakehurst/wiremock/jetty12/Jetty12MultipartParserLoader.java b/wiremock-jetty12/src/test/java/com/github/tomakehurst/wiremock/jetty12/Jetty12MultipartParserLoader.java new file mode 100644 index 0000000000..58608c4d33 --- /dev/null +++ b/wiremock-jetty12/src/test/java/com/github/tomakehurst/wiremock/jetty12/Jetty12MultipartParserLoader.java @@ -0,0 +1,32 @@ +/* + * Copyright (C) 2018-2024 Thomas Akehurst + * + * 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 + * + * http://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. + */ +package com.github.tomakehurst.wiremock.jetty12; + +import java.util.Optional; + +public class Jetty12MultipartParserLoader + implements com.github.tomakehurst.wiremock.MultipartParserLoader { + private static final String JETTY_12 = "12"; /* Jetty 12 */ + + @Override + public Optional getMultipartParser(String jettyMajorVersion) { + if (JETTY_12.equalsIgnoreCase(jettyMajorVersion)) { + return Optional.of(new Jetty12MultipartParser()); + } else { + return Optional.empty(); + } + } +} diff --git a/wiremock-jetty12/src/test/java/com/github/tomakehurst/wiremock/jetty12/servlet/AltHttpServerFactory.java b/wiremock-jetty12/src/test/java/com/github/tomakehurst/wiremock/jetty12/servlet/AltHttpServerFactory.java new file mode 100644 index 0000000000..2df2c6ba7b --- /dev/null +++ b/wiremock-jetty12/src/test/java/com/github/tomakehurst/wiremock/jetty12/servlet/AltHttpServerFactory.java @@ -0,0 +1,124 @@ +/* + * Copyright (C) 2016-2024 Thomas Akehurst + * + * 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 + * + * http://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. + */ +package com.github.tomakehurst.wiremock.jetty12.servlet; + +import static com.github.tomakehurst.wiremock.common.Exceptions.throwUnchecked; +import static com.github.tomakehurst.wiremock.core.WireMockApp.ADMIN_CONTEXT_ROOT; + +import com.github.tomakehurst.wiremock.common.ConsoleNotifier; +import com.github.tomakehurst.wiremock.common.FileSource; +import com.github.tomakehurst.wiremock.common.Notifier; +import com.github.tomakehurst.wiremock.core.Options; +import com.github.tomakehurst.wiremock.http.*; +import com.github.tomakehurst.wiremock.servlet.WireMockHandlerDispatchingServlet; +import org.eclipse.jetty.ee10.servlet.ServletContextHandler; +import org.eclipse.jetty.ee10.servlet.ServletHolder; +import org.eclipse.jetty.server.Handler; +import org.eclipse.jetty.server.Server; +import org.eclipse.jetty.server.ServerConnector; + +public class AltHttpServerFactory implements HttpServerFactory { + + @Override + public HttpServer buildHttpServer( + Options options, + AdminRequestHandler adminRequestHandler, + StubRequestHandler stubRequestHandler) { + + final Server jettyServer = new Server(0); + ConsoleNotifier notifier = new ConsoleNotifier(false); + ServletContextHandler adminContext = + addAdminContext(jettyServer, adminRequestHandler, notifier); + ServletContextHandler mockServiceContext = + addMockServiceContext(jettyServer, stubRequestHandler, options.filesRoot(), notifier); + + Handler.Abstract handler = new Handler.Sequence(adminContext, mockServiceContext); + jettyServer.setHandler(handler); + + return new HttpServer() { + + @Override + public void start() { + try { + jettyServer.start(); + } catch (Exception e) { + throwUnchecked(e); + } + } + + @Override + public void stop() { + try { + jettyServer.stop(); + } catch (Exception e) { + throwUnchecked(e); + } + } + + @Override + public boolean isRunning() { + return jettyServer.isRunning(); + } + + @Override + public int port() { + return ((ServerConnector) jettyServer.getConnectors()[0]).getLocalPort(); + } + + @Override + public int httpsPort() { + return 0; + } + }; + } + + @SuppressWarnings({"rawtypes", "unchecked"}) + private ServletContextHandler addMockServiceContext( + Server jettyServer, + StubRequestHandler stubRequestHandler, + FileSource fileSource, + Notifier notifier) { + ServletContextHandler mockServiceContext = new ServletContextHandler(); + mockServiceContext.setServer(jettyServer); + mockServiceContext.setContextPath("/"); + + mockServiceContext.setAttribute(StubRequestHandler.class.getName(), stubRequestHandler); + mockServiceContext.setAttribute(Notifier.KEY, notifier); + ServletHolder servletHolder = + mockServiceContext.addServlet(WireMockHandlerDispatchingServlet.class, "/"); + servletHolder.setInitParameter( + RequestHandler.HANDLER_CLASS_KEY, StubRequestHandler.class.getName()); + servletHolder.setInitParameter( + WireMockHandlerDispatchingServlet.SHOULD_FORWARD_TO_FILES_CONTEXT, "false"); + + return mockServiceContext; + } + + private ServletContextHandler addAdminContext( + Server jettyServer, AdminRequestHandler adminRequestHandler, Notifier notifier) { + ServletContextHandler adminContext = new ServletContextHandler(); + adminContext.setServer(jettyServer); + adminContext.setContextPath(ADMIN_CONTEXT_ROOT); + + ServletHolder servletHolder = + adminContext.addServlet(WireMockHandlerDispatchingServlet.class, "/"); + servletHolder.setInitParameter( + RequestHandler.HANDLER_CLASS_KEY, AdminRequestHandler.class.getName()); + adminContext.setAttribute(AdminRequestHandler.class.getName(), adminRequestHandler); + adminContext.setAttribute(Notifier.KEY, notifier); + return adminContext; + } +} diff --git a/wiremock-jetty12/src/test/java/com/github/tomakehurst/wiremock/jetty12/servlet/AlternativeServletContainerTest.java b/wiremock-jetty12/src/test/java/com/github/tomakehurst/wiremock/jetty12/servlet/AlternativeServletContainerTest.java new file mode 100644 index 0000000000..e174ac7a2d --- /dev/null +++ b/wiremock-jetty12/src/test/java/com/github/tomakehurst/wiremock/jetty12/servlet/AlternativeServletContainerTest.java @@ -0,0 +1,52 @@ +/* + * Copyright (C) 2016-2024 Thomas Akehurst + * + * 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 + * + * http://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. + */ +package com.github.tomakehurst.wiremock.jetty12.servlet; + +import static com.github.tomakehurst.wiremock.client.WireMock.*; +import static com.github.tomakehurst.wiremock.core.WireMockConfiguration.options; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.is; + +import com.github.tomakehurst.wiremock.client.WireMock; +import com.github.tomakehurst.wiremock.junit5.WireMockExtension; +import com.github.tomakehurst.wiremock.testsupport.WireMockTestClient; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +public class AlternativeServletContainerTest { + + @RegisterExtension + public WireMockExtension wm = + WireMockExtension.newInstance() + .options(options().dynamicPort().httpServerFactory(new AltHttpServerFactory())) + .build(); + + private WireMockTestClient client; + + @BeforeEach + public void init() { + client = new WireMockTestClient(wm.getPort()); + WireMock.configureFor(wm.getPort()); + } + + @Test + public void supportsAlternativeHttpServerForBasicStub() { + stubFor(get(urlEqualTo("/alt-server")).willReturn(aResponse().withStatus(204))); + + assertThat(client.get("/alt-server").statusCode(), is(204)); + } +}