feat: Use bouncycastle to generate certificates#3354
Conversation
| `maven-publish` | ||
| id("com.diffplug.spotless") | ||
| id("com.github.johnrengelman.shadow") | ||
| id("com.gradleup.shadow") |
There was a problem hiding this comment.
note: Need to switch to a more recent shadow jar release to support bouncycastle without issues.
|
Thanks for contributing this, we've been wanting to make this change for a long time. The biggest concern we've had about this is the ~10Mb it'll add to the standalone file size. Can you think of way we can mitigate this? The idea we were kicking around was to have a separate library just to do the fake CA stuff using Bouncycastle, and shrink it aggressively with Proguard (or similar). This is obviously a bit more hassle to maintain than keeping it in the core codebase, so we'd welcome any more convenient approaches. |
|
Using the minimize from shadow jar seems to work well the total size of the jar is around 25 MB including bouncycastle. Unminimized with bouncycastle its ~ 34 MB However it might delete some classes that are dynamically loaded, i tried a few requests and didn't find any case where it caused issues. |
|
I really want to merge this, but I'm nervous about applying One reasonable compromise might be to move all of the code relating to certificate wrangling to a dedicated sub-project, which would own the bouncycastle dependency. Then |
4911927 to
68fa15a
Compare
| [plugins] | ||
| nexus-publish = { id = "io.github.gradle-nexus.publish-plugin", version = "2.0.0" } | ||
| spotless = { id = "com.diffplug.spotless", version = "8.3.0" } | ||
| shadow = { id = "com.github.johnrengelman.shadow", version = "8.1.1" } |
There was a problem hiding this comment.
note: Actually unused, as the version is set in the buildSrc
| not( | ||
| assignableTo( | ||
| com.github.tomakehurst.wiremock.standalone.WireMockServerRunner.class))) | ||
| .and(not(ANONYMOUS_CLASSES)) |
There was a problem hiding this comment.
note: Locally the tests where flaky, and anonymous classes where detected as unused, and sometimes the test passed, I excluded anonymous classes
68fa15a to
f7177ee
Compare
| @@ -0,0 +1,95 @@ | |||
| plugins { | |||
There was a problem hiding this comment.
note: With the new version of shadow jar, there is an issue with maven publication when the main project has multiple publications
Execution failed for task ':generatePomFileForMavenJavaPublication'.
> Failed to query the value of property 'dependencies'.
> Publishing is not able to resolve a dependency on a project with multiple publications that have different coordinates.
Found the following publications in root project 'wiremock':
- Maven publication 'mavenJava' with coordinates org.wiremock:wiremock:4.0.0-beta.29
- Maven publication 'standaloneJar' with coordinates org.wiremock:wiremock-standalone:4.0.0-beta.29
So i had to extract the to a new submodule the standalone jar publication
|
@tomakehurst I extracted the certificate generation to a submodule, and running the publication i encountered some issues, so I had to extract the standalone publicaition to its own submodule. Maybe you have an idea to make it work in a simpler way ? |
| @@ -0,0 +1 @@ | |||
| java temurin-17.0.18+8 No newline at end of file | |||
There was a problem hiding this comment.
note: Asdf config file to set the correct java version, I added it as it helps to switch automatically to the right java version, feel free to tell me to remove it if you think its unecessary
e3a11cc to
a4bfcb1
Compare
a4bfcb1 to
513399d
Compare
|
Sorry for the delay reviewing this. I think this looks good, and having a sub-module for the standalone JAR is probably no bad thing. I'm doing some final eyeballing of the JAR, but will merge assuming that doesn't show up anything. @Mahoney can you think of any reason having a submodule for the standalone JAR would be a problem? I seem to remember us deciding not to do this in the distant past, but I don't know if any of the reasons are still valid. |
513399d to
bb8a369
Compare
|
I updated the |
# Conflicts: # gradle/libs.versions.toml diff --git c/build.gradle.kts i/build.gradle.kts index 34a79bc..f79dcb1 100644 --- c/build.gradle.kts +++ i/build.gradle.kts @@ -106,6 +106,7 @@ dependencies { testImplementation(libs.mockito.core) testImplementation(libs.mockito.junit.jupiter) testImplementation(libs.scala.library) + testImplementation(libs.bouncycastle.bcpkix) testRuntimeOnly(files("src/test/resources/classpath file source/classpathfiles.zip", "src/test/resources/classpath-filesource.jar")) testRuntimeOnly(files("test-extension/test-extension.jar")) @@ -385,7 +386,6 @@ eclipse.classpath.file { .filter { it.path.contains("JRE_CONTAINER") } .forEach { it.entryAttributes["module"] = true - it.entryAttributes["add-exports"] = "java.base/sun.security.x509=ALL-UNNAMED" } } } diff --git c/buildSrc/build.gradle.kts i/buildSrc/build.gradle.kts index 0477e81..c210374 100644 --- c/buildSrc/build.gradle.kts +++ i/buildSrc/build.gradle.kts @@ -11,7 +11,7 @@ repositories { dependencies { implementation("com.diffplug.gradle.spotless:com.diffplug.gradle.spotless.gradle.plugin:6.25.0") - implementation("com.github.johnrengelman.shadow:com.github.johnrengelman.shadow.gradle.plugin:8.1.1") + implementation("com.gradleup.shadow:com.gradleup.shadow.gradle.plugin:8.3.10") implementation("org.sonarqube:org.sonarqube.gradle.plugin:6.2.0.5505") implementation("com.vanniktech.maven.publish.base:com.vanniktech.maven.publish.base.gradle.plugin:0.35.0") } diff --git c/buildSrc/src/main/kotlin/wiremock.common-conventions.gradle.kts i/buildSrc/src/main/kotlin/wiremock.common-conventions.gradle.kts index 4d3a736..26f52ba 100644 --- c/buildSrc/src/main/kotlin/wiremock.common-conventions.gradle.kts +++ i/buildSrc/src/main/kotlin/wiremock.common-conventions.gradle.kts @@ -9,7 +9,7 @@ plugins { signing `maven-publish` id("com.diffplug.spotless") - id("com.github.johnrengelman.shadow") + id("com.gradleup.shadow") id("org.sonarqube") id("com.vanniktech.maven.publish.base") } @@ -30,7 +30,6 @@ java { tasks.jar { manifest { - attributes("Add-Exports" to "java.base/sun.security.x509") attributes("Implementation-Version" to project.version) attributes("Implementation-Title" to "WireMock") } @@ -44,7 +43,6 @@ tasks { options.encoding = "UTF-8" options.compilerArgs.addAll(listOf( "-XDenableSunApiLintControl", - "--add-exports=java.base/sun.security.x509=ALL-UNNAMED", )) } diff --git c/gradle/libs.versions.toml i/gradle/libs.versions.toml index e958b72..a49b8fd 100644 --- c/gradle/libs.versions.toml +++ i/gradle/libs.versions.toml @@ -11,6 +11,7 @@ mockito = "5.23.0" jmh = "1.37" apache-http5 = "5.4.3" archunit = "1.4.2" +bouncycastle = "1.84" [libraries] # Jetty dependencies @@ -130,6 +131,9 @@ jakarta-websockets = { module = "jakarta.websocket:jakarta.websocket-client-api" jspecify = { module = "org.jspecify:jspecify", version = "1.0.0" } +bouncycastle-bcpkix = { module = "org.bouncycastle:bcpkix-jdk18on", version.ref = "bouncycastle" } +bouncycastle-bcprov = { module = "org.bouncycastle:bcprov-jdk18on", version.ref = "bouncycastle" } + [plugins] nexus-publish = { id = "io.github.gradle-nexus.publish-plugin", version = "2.0.0" } spotless = { id = "com.diffplug.spotless", version = "8.4.0" } diff --git c/src/test/java/com/github/tomakehurst/wiremock/crypto/CertificateSpecification.java i/src/test/java/com/github/tomakehurst/wiremock/crypto/CertificateSpecification.java index ab4da08..5cd6cac 100644 --- c/src/test/java/com/github/tomakehurst/wiremock/crypto/CertificateSpecification.java +++ i/src/test/java/com/github/tomakehurst/wiremock/crypto/CertificateSpecification.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2020-2021 Thomas Akehurst + * Copyright (C) 2020-2026 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,13 +15,19 @@ */ package com.github.tomakehurst.wiremock.crypto; +import java.io.IOException; import java.security.InvalidKeyException; import java.security.KeyPair; import java.security.SignatureException; import java.security.cert.CertificateException; import java.security.cert.X509Certificate; +import org.bouncycastle.operator.OperatorCreationException; public interface CertificateSpecification { X509Certificate certificateFor(KeyPair keyPair) - throws CertificateException, InvalidKeyException, SignatureException; + throws CertificateException, + InvalidKeyException, + SignatureException, + IOException, + OperatorCreationException; } diff --git c/src/test/java/com/github/tomakehurst/wiremock/crypto/X509CertificateSpecification.java i/src/test/java/com/github/tomakehurst/wiremock/crypto/X509CertificateSpecification.java index 2297d48..8ca86b3 100644 --- c/src/test/java/com/github/tomakehurst/wiremock/crypto/X509CertificateSpecification.java +++ i/src/test/java/com/github/tomakehurst/wiremock/crypto/X509CertificateSpecification.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2020-2021 Thomas Akehurst + * Copyright (C) 2020-2026 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,99 +15,46 @@ */ package com.github.tomakehurst.wiremock.crypto; -import static com.github.tomakehurst.wiremock.common.Exceptions.throwUnchecked; import static java.util.Objects.requireNonNull; +import com.github.tomakehurst.wiremock.http.ssl.CertificateAuthority; import java.io.IOException; -import java.math.BigInteger; -import java.security.InvalidKeyException; import java.security.KeyPair; -import java.security.NoSuchAlgorithmException; -import java.security.NoSuchProviderException; -import java.security.SecureRandom; -import java.security.SignatureException; import java.security.cert.CertificateException; import java.security.cert.X509Certificate; import java.util.Date; -import sun.security.x509.AlgorithmId; -import sun.security.x509.CertificateAlgorithmId; -import sun.security.x509.CertificateIssuerName; -import sun.security.x509.CertificateSerialNumber; -import sun.security.x509.CertificateSubjectName; -import sun.security.x509.CertificateValidity; -import sun.security.x509.CertificateX509Key; -import sun.security.x509.X500Name; -import sun.security.x509.X509CertImpl; -import sun.security.x509.X509CertInfo; +import javax.security.auth.x500.X500Principal; +import org.bouncycastle.operator.OperatorCreationException; -@SuppressWarnings("sunapi") public class X509CertificateSpecification implements CertificateSpecification { - private final X509CertificateVersion version; - private final X500Name subject; - private final X500Name issuer; + private final String subject; + private final String issuer; // java.time is JDK8 only private final Date notBefore; private final Date notAfter; - public X509CertificateSpecification( - X509CertificateVersion version, String subject, String issuer, Date notBefore, Date notAfter) + public X509CertificateSpecification(String subject, String issuer, Date notBefore, Date notAfter) throws IOException { - this.version = requireNonNull(version); - this.subject = new X500Name(requireNonNull(subject)); - this.issuer = new X500Name(requireNonNull(issuer)); + this.subject = requireNonNull(subject); + this.issuer = requireNonNull(issuer); this.notBefore = requireNonNull(notBefore); this.notAfter = requireNonNull(notAfter); } @OverRide public X509Certificate certificateFor(KeyPair keyPair) - throws CertificateException, InvalidKeyException, SignatureException { - try { - SecureRandom random = new SecureRandom(); - - X509CertInfo info = new X509CertInfo(); - info.set(X509CertInfo.VERSION, version.getVersion()); - - // On Java <= 1.7 it has to be a `CertificateSubjectName` - // On Java >= 1.8 it has to be an `X500Name` - try { - info.set(X509CertInfo.SUBJECT, subject); - } catch (CertificateException ignore) { - info.set(X509CertInfo.SUBJECT, new CertificateSubjectName(subject)); - } - - // On Java <= 1.7 it has to be a `CertificateIssuerName` - // On Java >= 1.8 it has to be an `X500Name` - try { - info.set(X509CertInfo.ISSUER, issuer); - } catch (CertificateException ignore) { - info.set(X509CertInfo.ISSUER, new CertificateIssuerName(issuer)); - } - - info.set(X509CertInfo.VALIDITY, new CertificateValidity(notBefore, notAfter)); - - info.set(X509CertInfo.KEY, new CertificateX509Key(keyPair.getPublic())); - info.set(X509CertInfo.SERIAL_NUMBER, new CertificateSerialNumber(new BigInteger(64, random))); - info.set( - X509CertInfo.ALGORITHM_ID, - new CertificateAlgorithmId(new AlgorithmId(AlgorithmId.SHA256_oid))); - - // Sign the cert to identify the algorithm that's used. - X509CertImpl cert = new X509CertImpl(info); - cert.sign(keyPair.getPrivate(), "SHA256withRSA"); - - // Update the algorithm and sign again. - info.set( - CertificateAlgorithmId.NAME + '.' + CertificateAlgorithmId.ALGORITHM, - cert.get(X509CertImpl.SIG_ALG)); - cert = new X509CertImpl(info); - cert.sign(keyPair.getPrivate(), "SHA256withRSA"); - cert.verify(keyPair.getPublic()); - - return cert; - } catch (IOException | NoSuchAlgorithmException | NoSuchProviderException e) { - return throwUnchecked(e, null); - } + throws CertificateException, IOException, OperatorCreationException { + return CertificateAuthority.buildCertificate( + CertificateAuthority.SIG_ALG_PREFIX + "RSA", + keyPair.getPublic(), + keyPair.getPrivate(), + keyPair.getPublic(), + notBefore, + notAfter, + new X500Principal(subject), + new X500Principal(issuer), + null, + false); } } diff --git c/src/test/java/com/github/tomakehurst/wiremock/crypto/X509CertificateVersion.java i/src/test/java/com/github/tomakehurst/wiremock/crypto/X509CertificateVersion.java deleted file mode 100644 index e5907d2..000000000 --- c/src/test/java/com/github/tomakehurst/wiremock/crypto/X509CertificateVersion.java +++ /dev/null @@ -1,46 +0,0 @@ -/* - * Copyright (C) 2020-2021 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.crypto; - -import static com.github.tomakehurst.wiremock.common.Exceptions.throwUnchecked; - -import java.io.IOException; -import sun.security.x509.CertificateVersion; - -@SuppressWarnings("sunapi") -public enum X509CertificateVersion { - V1(CertificateVersion.V1), - V2(CertificateVersion.V2), - V3(CertificateVersion.V3); - - private final CertificateVersion version; - - X509CertificateVersion(int version) { - this.version = getVersion(version); - } - - private static CertificateVersion getVersion(int version) { - try { - return new CertificateVersion(version); - } catch (IOException e) { - return throwUnchecked(e, null); - } - } - - CertificateVersion getVersion() { - return version; - } -} diff --git c/src/test/java/com/github/tomakehurst/wiremock/http/HttpClientFactoryCertificateVerificationTest.java i/src/test/java/com/github/tomakehurst/wiremock/http/HttpClientFactoryCertificateVerificationTest.java index 93ef47f..cf60944 100644 --- c/src/test/java/com/github/tomakehurst/wiremock/http/HttpClientFactoryCertificateVerificationTest.java +++ i/src/test/java/com/github/tomakehurst/wiremock/http/HttpClientFactoryCertificateVerificationTest.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2020-2025 Thomas Akehurst + * Copyright (C) 2020-2026 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,7 +18,6 @@ package com.github.tomakehurst.wiremock.http; import static com.github.tomakehurst.wiremock.common.ProxySettings.NO_PROXY; import static com.github.tomakehurst.wiremock.core.WireMockConfiguration.options; import static com.github.tomakehurst.wiremock.crypto.InMemoryKeyStore.KeyStoreType.JKS; -import static com.github.tomakehurst.wiremock.crypto.X509CertificateVersion.V3; import static java.util.Collections.emptyList; import com.github.tomakehurst.wiremock.WireMockServer; @@ -55,7 +54,6 @@ public abstract class HttpClientFactoryCertificateVerificationTest { CertificateSpecification certificateSpecification = new X509CertificateSpecification( - /* version= */ V3, /* subject= */ "CN=" + certificateCN, /* issuer= */ "CN=wiremock.org", /* notBefore= */ new Date(), diff --git c/src/test/java/com/github/tomakehurst/wiremock/http/ProxyResponseRendererTest.java i/src/test/java/com/github/tomakehurst/wiremock/http/ProxyResponseRendererTest.java index 5472cb6..7d9c4a2 100644 --- c/src/test/java/com/github/tomakehurst/wiremock/http/ProxyResponseRendererTest.java +++ i/src/test/java/com/github/tomakehurst/wiremock/http/ProxyResponseRendererTest.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2020-2025 Thomas Akehurst + * Copyright (C) 2020-2026 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,7 +17,6 @@ package com.github.tomakehurst.wiremock.http; 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; @@ -564,7 +563,6 @@ public class ProxyResponseRendererTest { CertificateSpecification certificateSpecification = new X509CertificateSpecification( - /* version= */ V3, /* subject= */ "CN=localhost", /* issuer= */ "CN=wiremock.org", /* notBefore= */ new Date(), diff --git c/wiremock-core/build.gradle.kts i/wiremock-core/build.gradle.kts index 6cada0d..333b253 100644 --- c/wiremock-core/build.gradle.kts +++ i/wiremock-core/build.gradle.kts @@ -26,6 +26,8 @@ dependencies { api(libs.jspecify) + api(libs.bouncycastle.bcpkix) + implementation(libs.apache.http5.client) implementation(libs.handlebars.helpers) { exclude(group = "org.mozilla", module = "rhino") @@ -42,6 +44,8 @@ dependencies { } implementation(libs.xmlunit.placeholders) + implementation(libs.bouncycastle.bcprov) + modules { module("org.apache.logging.log4j:log4j-core") { replacedBy("org.apache.logging.log4j:log4j-to-slf4j") diff --git c/wiremock-core/src/main/java/com/github/tomakehurst/wiremock/http/ssl/CertificateAuthority.java i/wiremock-core/src/main/java/com/github/tomakehurst/wiremock/http/ssl/CertificateAuthority.java index a43b7f8..1829fcf 100644 --- c/wiremock-core/src/main/java/com/github/tomakehurst/wiremock/http/ssl/CertificateAuthority.java +++ i/wiremock-core/src/main/java/com/github/tomakehurst/wiremock/http/ssl/CertificateAuthority.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2020-2025 Thomas Akehurst + * Copyright (C) 2020-2026 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,22 +16,46 @@ package com.github.tomakehurst.wiremock.http.ssl; import static com.github.tomakehurst.wiremock.common.ArrayFunctions.prepend; -import static com.github.tomakehurst.wiremock.common.Exceptions.throwUnchecked; import static java.util.Objects.requireNonNull; import java.io.IOException; -import java.security.*; +import java.math.BigInteger; +import java.security.KeyPair; +import java.security.KeyPairGenerator; +import java.security.NoSuchAlgorithmException; +import java.security.PrivateKey; +import java.security.PublicKey; +import java.security.SecureRandom; import java.security.cert.CertificateException; import java.security.cert.X509Certificate; import java.time.Period; import java.time.ZonedDateTime; import java.util.Date; import javax.net.ssl.SNIHostName; -import sun.security.x509.*; +import javax.security.auth.x500.X500Principal; +import org.bouncycastle.asn1.x509.BasicConstraints; +import org.bouncycastle.asn1.x509.Extension; +import org.bouncycastle.asn1.x509.GeneralName; +import org.bouncycastle.asn1.x509.GeneralNames; +import org.bouncycastle.asn1.x509.KeyUsage; +import org.bouncycastle.asn1.x509.SubjectPublicKeyInfo; +import org.bouncycastle.cert.X509CertificateHolder; +import org.bouncycastle.cert.X509v3CertificateBuilder; +import org.bouncycastle.cert.bc.BcX509ExtensionUtils; +import org.bouncycastle.cert.jcajce.JcaX509CertificateConverter; +import org.bouncycastle.cert.jcajce.JcaX509v3CertificateBuilder; +import org.bouncycastle.operator.ContentSigner; +import org.bouncycastle.operator.OperatorCreationException; +import org.bouncycastle.operator.jcajce.JcaContentSignerBuilder; +import org.jspecify.annotations.Nullable; -@SuppressWarnings("sunapi") public class CertificateAuthority { + public static final String SIG_ALG_PREFIX = "SHA256With"; + private static final Period CA_VALIDITY = Period.ofYears(10); + private static final Period CERT_VALIDITY = Period.ofYears(1); + public static final String CA_SUBJECT = "CN=WireMock Local Self Signed Root Certificate"; + private final X509Certificate[] certificateChain; private final PrivateKey key; @@ -46,72 +70,34 @@ public class CertificateAuthority { public static CertificateAuthority generateCertificateAuthority() throws CertificateGenerationUnsupportedException { try { - KeyPair pair = generateKeyPair("RSA"); - String sigAlg = "SHA256WithRSA"; - X509CertInfo info = - makeX509CertInfo( - sigAlg, - "WireMock Local Self Signed Root Certificate", - ZonedDateTime.now().minus(Period.ofDays(1)), - Period.ofYears(10), + String keyType = "RSA"; + KeyPair pair = generateKeyPair(keyType); + var certificate = + buildCertificate( + SIG_ALG_PREFIX + keyType, pair.getPublic(), - certificateAuthorityExtensions(pair.getPublic())); - - X509CertImpl certificate = selfSign(info, pair.getPrivate(), sigAlg); + pair.getPrivate(), + pair.getPublic(), + CA_VALIDITY, + new X500Principal(CA_SUBJECT), + new X500Principal(CA_SUBJECT), + null, + true); return new CertificateAuthority(new X509Certificate[] {certificate}, pair.getPrivate()); } catch (NoSuchAlgorithmException - | NoSuchProviderException - | InvalidKeyException | CertificateException - | SignatureException + | IOException + | OperatorCreationException | NoSuchMethodError | VerifyError | NoClassDefFoundError - | IOException | IllegalAccessError e) { throw new CertificateGenerationUnsupportedException( "Your runtime does not support generating certificates at runtime", e); } } - private static X509CertImpl selfSign(X509CertInfo info, PrivateKey privateKey, String sigAlg) - throws CertificateException, - NoSuchAlgorithmException, - InvalidKeyException, - NoSuchProviderException, - SignatureException { - X509CertImpl certificate = new X509CertImpl(info); - certificate.sign(privateKey, sigAlg); - return certificate; - } - - private static CertificateExtensions certificateAuthorityExtensions(PublicKey publicKey) { - try { - KeyIdentifier keyId = new KeyIdentifier(publicKey); - byte[] keyIdBytes = keyId.getIdentifier(); - CertificateExtensions extensions = new CertificateExtensions(); - extensions.set( - AuthorityKeyIdentifierExtension.NAME, - new AuthorityKeyIdentifierExtension(keyId, null, null)); - - extensions.set( - BasicConstraintsExtension.NAME, new BasicConstraintsExtension(true, Integer.MAX_VALUE)); - - KeyUsageExtension keyUsage = new KeyUsageExtension(new boolean[7]); - keyUsage.set(KeyUsageExtension.KEY_CERTSIGN, true); - keyUsage.set(KeyUsageExtension.CRL_SIGN, true); - extensions.set(KeyUsageExtension.NAME, keyUsage); - - extensions.set( - SubjectKeyIdentifierExtension.NAME, new SubjectKeyIdentifierExtension(keyIdBytes)); - - return extensions; - } catch (IOException e) { - return throwUnchecked(e, null); - } - } - public X509Certificate[] certificateChain() { return certificateChain; } @@ -124,106 +110,112 @@ public class CertificateAuthority { throws CertificateGenerationUnsupportedException { try { KeyPair pair = generateKeyPair(keyType); - String sigAlg = "SHA256With" + keyType; - X509CertInfo info = - makeX509CertInfo( - sigAlg, - hostName.getAsciiName(), - ZonedDateTime.now().minus(Period.ofDays(1)), - Period.ofYears(1), + X509Certificate issuer = certificateChain[0]; + + var certificate = + buildCertificate( + SIG_ALG_PREFIX + keyType, pair.getPublic(), - subjectAlternativeName(hostName)); + key, + issuer.getPublicKey(), + CERT_VALIDITY, + new X500Principal("CN=" + hostName.getAsciiName()), + issuer.getIssuerX500Principal(), + hostName.getAsciiName(), + false); - X509CertImpl certificate = sign(info); - - X509Certificate[] fullChain = prepend(certificate, certificateChain); - return new CertChainAndKey(fullChain, pair.getPrivate()); + return new CertChainAndKey(prepend(certificate, certificateChain), pair.getPrivate()); } catch (NoSuchAlgorithmException - | NoSuchProviderException - | InvalidKeyException | CertificateException - | SignatureException + | IOException + | OperatorCreationException | NoSuchMethodError | VerifyError | NoClassDefFoundError - | IOException | IllegalAccessError e) { throw new CertificateGenerationUnsupportedException( "Your runtime does not support generating certificates at runtime", e); } } - private X509CertImpl sign(X509CertInfo info) - throws CertificateException, - IOException, - NoSuchAlgorithmException, - InvalidKeyException, - NoSuchProviderException, - SignatureException { - X509Certificate issuerCertificate = certificateChain[0]; - info.set(X509CertInfo.ISSUER, issuerCertificate.getSubjectDN()); - - X509CertImpl certificate = new X509CertImpl(info); - certificate.sign(key, issuerCertificate.getSigAlgName()); - return certificate; - } - private static KeyPair generateKeyPair(String keyType) throws NoSuchAlgorithmException { KeyPairGenerator keyGen = KeyPairGenerator.getInstance(keyType); keyGen.initialize(2048, new SecureRandom()); return keyGen.generateKeyPair(); } - private static X509CertInfo makeX509CertInfo( + public static X509Certificate buildCertificate( String sigAlg, - String subjectName, - ZonedDateTime start, - Period validity, PublicKey publicKey, - CertificateExtensions certificateExtensions) - throws IOException, CertificateException, NoSuchAlgorithmException { - ZonedDateTime end = start.plus(validity); - - X500Name myname = new X500Name("CN=" + subjectName); - X509CertInfo info = new X509CertInfo(); - // Add all mandatory attributes - info.set(X509CertInfo.VERSION, new CertificateVersion(CertificateVersion.V3)); - info.set( - X509CertInfo.SERIAL_NUMBER, - new CertificateSerialNumber(new java.util.Random().nextInt() & 0x7fffffff)); - info.set(X509CertInfo.ALGORITHM_ID, new CertificateAlgorithmId(AlgorithmId.get(sigAlg))); - info.set(X509CertInfo.SUBJECT, myname); - info.set(X509CertInfo.KEY, new CertificateX509Key(publicKey)); - info.set( - X509CertInfo.VALIDITY, - new CertificateValidity(Date.from(start.toInstant()), Date.from(end.toInstant()))); - info.set(X509CertInfo.ISSUER, myname); - info.set(X509CertInfo.EXTENSIONS, certificateExtensions); - return info; + PrivateKey signerPrivateKey, + PublicKey signerPublicKey, + Period validity, + X500Principal subject, + X500Principal issuer, + @nullable String sanDnsName, + boolean isCA) + throws IOException, OperatorCreationException, CertificateException { + ZonedDateTime start = ZonedDateTime.now().minus(Period.ofDays(1)); + return buildCertificate( + sigAlg, + publicKey, + signerPrivateKey, + signerPublicKey, + Date.from(start.toInstant()), + Date.from(start.plus(validity).toInstant()), + subject, + issuer, + sanDnsName, + isCA); } - private static CertificateExtensions subjectAlternativeName(SNIHostName hostName) { - GeneralName name = new GeneralName(dnsName(hostName)); - GeneralNames names = new GeneralNames(); - names.add(name); - try { - CertificateExtensions extensions = new CertificateExtensions(); - extensions.set( - SubjectAlternativeNameExtension.NAME, new SubjectAlternativeNameExtension(names)); - return extensions; - } catch (IOException e) { - // it's an in memory op, should be impossible... - return throwUnchecked(e, null); - } - } + public static X509Certificate buildCertificate( + String sigAlg, + PublicKey publicKey, + PrivateKey signerPrivateKey, + PublicKey signerPublicKey, + Date notBefore, + Date notAfter, + X500Principal subject, + X500Principal issuer, + @nullable String sanDnsName, + boolean isCA) + throws IOException, CertificateException, OperatorCreationException { + SubjectPublicKeyInfo subjectKeyInfo = SubjectPublicKeyInfo.getInstance(publicKey.getEncoded()); + SubjectPublicKeyInfo signerKeyInfo = + SubjectPublicKeyInfo.getInstance(signerPublicKey.getEncoded()); - private static DNSName dnsName(SNIHostName name) { - try { - return new DNSName(name.getAsciiName()); - } catch (IOException e) { - // DNSName throws IOException for a parse error (which isn't an IO problem...) - // An SNIHostName should be guaranteed not to have a parse issue - return throwUnchecked(e, null); + X509v3CertificateBuilder builder = + new JcaX509v3CertificateBuilder( + issuer, + BigInteger.valueOf(new SecureRandom().nextLong()).abs(), + notBefore, + notAfter, + subject, + publicKey); + + BcX509ExtensionUtils extUtils = new BcX509ExtensionUtils(); + builder.addExtension( + Extension.subjectKeyIdentifier, false, extUtils.createSubjectKeyIdentifier(subjectKeyInfo)); + builder.addExtension( + Extension.authorityKeyIdentifier, + false, + extUtils.createAuthorityKeyIdentifier(signerKeyInfo)); + + if (isCA) { + builder.addExtension(Extension.basicConstraints, true, new BasicConstraints(true)); + builder.addExtension( + Extension.keyUsage, true, new KeyUsage(KeyUsage.keyCertSign | KeyUsage.cRLSign)); } + + if (sanDnsName != null) { + builder.addExtension( + Extension.subjectAlternativeName, + false, + new GeneralNames(new GeneralName(GeneralName.dNSName, sanDnsName))); + } + ContentSigner signer = new JcaContentSignerBuilder(sigAlg).build(signerPrivateKey); + X509CertificateHolder holder = builder.build(signer); + return new JcaX509CertificateConverter().getCertificate(holder); } }
diff --git c/.tool-versions i/.tool-versions new file mode 100644 index 000000000..01ad62a --- /dev/null +++ i/.tool-versions @@ -0,0 +1 @@ +java temurin-17.0.18+8 \ No newline at end of file diff --git c/build.gradle.kts i/build.gradle.kts index f79dcb1..2cb691b 100644 --- c/build.gradle.kts +++ i/build.gradle.kts @@ -188,56 +188,6 @@ tasks.jar { } } -tasks.shadowJar { - archiveBaseName = "wiremock-standalone" - archiveClassifier = "" - configurations = listOf( - project.configurations.runtimeClasspath.get(), - ) - - relocate("org.mortbay", "wiremock.org.mortbay") - relocate("org.eclipse", "wiremock.org.eclipse") - relocate("org.codehaus", "wiremock.org.codehaus") - relocate("com.google", "wiremock.com.google") - relocate("com.google.thirdparty", "wiremock.com.google.thirdparty") - relocate("com.fasterxml.jackson", "wiremock.com.fasterxml.jackson") - relocate("org.apache", "wiremock.org.apache") - relocate("org.xmlunit", "wiremock.org.xmlunit") - relocate("org.hamcrest", "wiremock.org.hamcrest") - relocate("org.skyscreamer", "wiremock.org.skyscreamer") - relocate("org.json", "wiremock.org.json") - relocate("net.minidev", "wiremock.net.minidev") - relocate("com.jayway", "wiremock.com.jayway") - relocate("org.objectweb", "wiremock.org.objectweb") - relocate("org.custommonkey", "wiremock.org.custommonkey") - relocate("net.javacrumbs", "wiremock.net.javacrumbs") - relocate("net.sf", "wiremock.net.sf") - relocate("com.github.jknack", "wiremock.com.github.jknack") - relocate("org.antlr", "wiremock.org.antlr") - relocate("jakarta.servlet", "wiremock.jakarta.servlet") - relocate("org.checkerframework", "wiremock.org.checkerframework") - relocate("org.hamcrest", "wiremock.org.hamcrest") - relocate("org.slf4j", "wiremock.org.slf4j") - relocate("joptsimple", "wiremock.joptsimple") - exclude("joptsimple/HelpFormatterMessages.properties") - relocate("org.yaml", "wiremock.org.yaml") - relocate("com.ethlo", "wiremock.com.ethlo") - relocate("com.networknt", "wiremock.com.networknt") - relocate("org.jspecify", "wiremock.org.jspecify") - - dependencies { - exclude(dependency("junit:junit")) - } - - mergeServiceFiles() - - exclude("META-INF/maven/**") - exclude("META-INF/versions/17/**") - exclude("META-INF/versions/21/**") - exclude("META-INF/versions/22/**") - exclude("module-info.class") - exclude("handlebars-*.js") -} publishing { publications { @@ -251,21 +201,6 @@ publishing { description = "A web service test double for all occasions" } } - - create<MavenPublication>("standaloneJar") { - artifactId = "${tasks.jar.get().archiveBaseName.get()}-standalone" - project.shadow.component(this) - - artifact(tasks.named("sourcesJar")) - artifact(tasks.named("javadocJar")) - artifact(testJar) - - pom.packaging = "jar" - pom { - name = "WireMock" - description = "A web service test double for all occasions - standalone edition" - } - } } } @@ -291,7 +226,6 @@ val addGitTag by tasks.registering { tasks.publish { dependsOn( checkReleasePreconditions, - "signStandaloneJarPublication", "signMavenJavaPublication", ) } @@ -301,7 +235,7 @@ tasks.withType<AbstractPublishToMaven>().configureEach { } tasks.assemble { - dependsOn(tasks.jar, tasks.shadowJar) + dependsOn(tasks.jar) } tasks.register("release") { diff --git c/buildSrc/build.gradle.kts i/buildSrc/build.gradle.kts index c210374..ae30478 100644 --- c/buildSrc/build.gradle.kts +++ i/buildSrc/build.gradle.kts @@ -11,7 +11,7 @@ repositories { dependencies { implementation("com.diffplug.gradle.spotless:com.diffplug.gradle.spotless.gradle.plugin:6.25.0") - implementation("com.gradleup.shadow:com.gradleup.shadow.gradle.plugin:8.3.10") + implementation("com.gradleup.shadow:com.gradleup.shadow.gradle.plugin:9.2.2") implementation("org.sonarqube:org.sonarqube.gradle.plugin:6.2.0.5505") implementation("com.vanniktech.maven.publish.base:com.vanniktech.maven.publish.base.gradle.plugin:0.35.0") } diff --git c/buildSrc/src/main/kotlin/wiremock.common-conventions.gradle.kts i/buildSrc/src/main/kotlin/wiremock.common-conventions.gradle.kts index 9b5e600..88b3a5e 100644 --- c/buildSrc/src/main/kotlin/wiremock.common-conventions.gradle.kts +++ i/buildSrc/src/main/kotlin/wiremock.common-conventions.gradle.kts @@ -206,12 +206,6 @@ publishing { } } - getComponents().withType<AdhocComponentWithVariants>().forEach { c -> - c.withVariantsFromConfiguration(configurations.shadowRuntimeElements.get()) { - skip() - } - } - publications { withType<MavenPublication> { pom { @@ -223,6 +217,10 @@ publishing { } } +shadow { + addShadowVariantIntoJavaComponent = false +} + mavenPublishing { publishToMavenCentral(automaticRelease = true) } diff --git c/gradle/libs.versions.toml i/gradle/libs.versions.toml index 521f0b5..8cf8531 100644 --- c/gradle/libs.versions.toml +++ i/gradle/libs.versions.toml @@ -137,7 +137,6 @@ bouncycastle-bcprov = { module = "org.bouncycastle:bcprov-jdk18on", version.ref [plugins] nexus-publish = { id = "io.github.gradle-nexus.publish-plugin", version = "2.0.0" } spotless = { id = "com.diffplug.spotless", version = "8.3.0" } -shadow = { id = "com.github.johnrengelman.shadow", version = "8.1.1" } sonarqube = { id = "org.sonarqube", version = "7.2.3.7755" } jmh = { id = "me.champeau.jmh", version = "0.7.3" } -task-tree = { id = "com.dorongold.task-tree", version = "4.0.1" } +task-tree = { id = "com.dorongold.task-tree", version = "4.0.1" } \ No newline at end of file diff --git c/settings.gradle.kts i/settings.gradle.kts index 4d79e1c..4e7d94f 100644 --- c/settings.gradle.kts +++ i/settings.gradle.kts @@ -5,6 +5,8 @@ plugins { rootProject.name = "wiremock" include("wiremock-core") +include("wiremock-core:certificate-generator") +include("wiremock-standalone") include("wiremock-junit4") include("wiremock-junit5") include("wiremock-jetty") diff --git c/src/test/java/com/github/tomakehurst/wiremock/common/ArrayFunctionsTest.java i/src/test/java/com/github/tomakehurst/wiremock/common/ArrayFunctionsTest.java index 950f6c3..37149bb 100644 --- c/src/test/java/com/github/tomakehurst/wiremock/common/ArrayFunctionsTest.java +++ i/src/test/java/com/github/tomakehurst/wiremock/common/ArrayFunctionsTest.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2020-2025 Thomas Akehurst + * Copyright (C) 2020-2026 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 static com.github.tomakehurst.wiremock.common.ArrayFunctions.concat; -import static com.github.tomakehurst.wiremock.common.ArrayFunctions.prepend; import static org.junit.jupiter.api.Assertions.assertArrayEquals; import org.junit.jupiter.api.Test; @@ -64,37 +63,4 @@ class ArrayFunctionsTest { second[0] = 30; assertArrayEquals(new Integer[] {1, 2, 3, 4}, result); } - - @test - void prependNullAndEmpty() { - assertArrayEquals(new Integer[] {null}, prepend(null, empty)); - } - - @test - void prependSomeAndEmpty() { - Integer[] result = prepend(1, empty); - assertArrayEquals(new Integer[] {1}, result); - } - - @test - void prependNullAndNonEmpty() { - Integer[] second = {1, 2}; - - Integer[] result = prepend(null, second); - assertArrayEquals(new Integer[] {null, 1, 2}, result); - - second[0] = 10; - assertArrayEquals(new Integer[] {null, 1, 2}, result); - } - - @test - void prependSomeAndNonEmpty() { - Integer[] second = {2, 3}; - - Integer[] result = prepend(1, second); - assertArrayEquals(new Integer[] {1, 2, 3}, result); - - second[0] = 30; - assertArrayEquals(new Integer[] {1, 2, 3}, result); - } } diff --git c/src/test/java/com/github/tomakehurst/wiremock/http/ssl/CertificateAuthorityTest.java i/src/test/java/com/github/tomakehurst/wiremock/http/ssl/CertificateAuthorityTest.java new file mode 100644 index 000000000..eb45608 --- /dev/null +++ i/src/test/java/com/github/tomakehurst/wiremock/http/ssl/CertificateAuthorityTest.java @@ -0,0 +1,59 @@ +/* + * Copyright (C) 2026 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.http.ssl; + +import static com.github.tomakehurst.wiremock.http.ssl.CertificateAuthority.prepend; +import static org.junit.jupiter.api.Assertions.*; + +import org.junit.jupiter.api.Test; + +class CertificateAuthorityTest { + + private final Integer[] empty = new Integer[0]; + + @test + void prependNullAndEmpty() { + assertArrayEquals(new Integer[] {null}, prepend(null, empty)); + } + + @test + void prependSomeAndEmpty() { + Integer[] result = prepend(1, empty); + assertArrayEquals(new Integer[] {1}, result); + } + + @test + void prependNullAndNonEmpty() { + Integer[] second = {1, 2}; + + Integer[] result = prepend(null, second); + assertArrayEquals(new Integer[] {null, 1, 2}, result); + + second[0] = 10; + assertArrayEquals(new Integer[] {null, 1, 2}, result); + } + + @test + void prependSomeAndNonEmpty() { + Integer[] second = {2, 3}; + + Integer[] result = prepend(1, second); + assertArrayEquals(new Integer[] {1, 2, 3}, result); + + second[0] = 30; + assertArrayEquals(new Integer[] {1, 2, 3}, result); + } +} diff --git c/src/test/java/com/github/tomakehurst/wiremock/jetty/archunit/UnusedCodeTest.java i/src/test/java/com/github/tomakehurst/wiremock/jetty/archunit/UnusedCodeTest.java index e950cbd..3b8e33d 100644 --- c/src/test/java/com/github/tomakehurst/wiremock/jetty/archunit/UnusedCodeTest.java +++ i/src/test/java/com/github/tomakehurst/wiremock/jetty/archunit/UnusedCodeTest.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2021-2025 Thomas Akehurst + * Copyright (C) 2021-2026 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 @@ package com.github.tomakehurst.wiremock.jetty.archunit; import static com.tngtech.archunit.base.DescribedPredicate.describe; import static com.tngtech.archunit.base.DescribedPredicate.not; +import static com.tngtech.archunit.core.domain.JavaClass.Predicates.ANONYMOUS_CLASSES; import static com.tngtech.archunit.core.domain.JavaClass.Predicates.assignableTo; import static com.tngtech.archunit.core.domain.JavaMember.Predicates.declaredIn; import static com.tngtech.archunit.core.domain.properties.HasName.Utils.namesOf; @@ -81,6 +82,7 @@ class UnusedCodeTest { not( assignableTo( com.github.tomakehurst.wiremock.standalone.WireMockServerRunner.class))) + .and(not(ANONYMOUS_CLASSES)) .should(beReferencedClass) .as("should use all classes") .because("unused classes should be removed")); diff --git c/src/test/resources/frozen/unused-classes i/src/test/resources/frozen/unused-classes index a1f3bbc..e122073 100644 --- c/src/test/resources/frozen/unused-classes +++ i/src/test/resources/frozen/unused-classes @@ -1,7 +1 @@ -Class <com.github.tomakehurst.wiremock.client.WireMock$2> is unreferenced in (WireMock.java:0) -Class <com.github.tomakehurst.wiremock.common.ContentTypes$1> is unreferenced in (ContentTypes.java:0) -Class <com.github.tomakehurst.wiremock.common.xml.XmlNode$1> is unreferenced in (XmlNode.java:0) -Class <com.github.tomakehurst.wiremock.extension.ServeEventListener$1> is unreferenced in (ServeEventListener.java:0) -Class <com.github.tomakehurst.wiremock.extension.responsetemplating.helpers.FormatJsonHelper$1> is unreferenced in (FormatJsonHelper.java:0) -Class <com.github.tomakehurst.wiremock.extension.responsetemplating.helpers.FormatXmlHelper$1> is unreferenced in (FormatXmlHelper.java:0) -Class <com.github.tomakehurst.wiremock.http.JvmProxyConfigurer> is unreferenced in (JvmProxyConfigurer.java:0) \ No newline at end of file +Class <com.github.tomakehurst.wiremock.http.JvmProxyConfigurer> is unreferenced in (JvmProxyConfigurer.java:0) diff --git c/wiremock-core/build.gradle.kts i/wiremock-core/build.gradle.kts index 333b253..6f4e5e0 100644 --- c/wiremock-core/build.gradle.kts +++ i/wiremock-core/build.gradle.kts @@ -26,7 +26,7 @@ dependencies { api(libs.jspecify) - api(libs.bouncycastle.bcpkix) + api(project(":wiremock-core:certificate-generator")) implementation(libs.apache.http5.client) implementation(libs.handlebars.helpers) { @@ -44,7 +44,6 @@ dependencies { } implementation(libs.xmlunit.placeholders) - implementation(libs.bouncycastle.bcprov) modules { module("org.apache.logging.log4j:log4j-core") { diff --git c/wiremock-core/certificate-generator/build.gradle.kts i/wiremock-core/certificate-generator/build.gradle.kts new file mode 100644 index 000000000..65afdcc --- /dev/null +++ i/wiremock-core/certificate-generator/build.gradle.kts @@ -0,0 +1,53 @@ +plugins { + id("wiremock.common-conventions") + id("com.gradleup.shadow") +} + +tasks.shadowJar { + archiveClassifier = "" + description = "Create a shadow JAR of all dependencies" + minimize() + configurations = listOf( + project.configurations.compileClasspath.get(), + ) +} + +tasks.jar { + enabled = false +} + +shadow { + addShadowVariantIntoJavaComponent = true +} + +dependencies { + // As we include them in the shadowed jar to reduce the size, we must not expose them to + // our consumers as transitive dependencies + compileOnly(libs.bouncycastle.bcpkix) + compileOnly(libs.bouncycastle.bcprov) +} + +publishing { + publications { + create<MavenPublication>("shadow") { + artifactId = tasks.shadowJar.get().archiveBaseName.get() + from(components["shadow"]) + artifact(tasks.sourcesJar) + pom { + name = "WireMock Bouncy Castle Wrapper" + description = "A bouncy castle wrapper with reduced size" + } + } + } +} + +// Disable the plain jar from being published/consumed +configurations.apiElements { + outgoing.artifacts.clear() + outgoing.artifact(tasks.shadowJar) +} + +configurations.runtimeElements { + outgoing.artifacts.clear() + outgoing.artifact(tasks.shadowJar) +} diff --git c/wiremock-core/src/main/java/com/github/tomakehurst/wiremock/http/ssl/CertChainAndKey.java i/wiremock-core/certificate-generator/src/main/java/com/github/tomakehurst/wiremock/http/ssl/CertChainAndKey.java similarity index 95% rename from wiremock-core/src/main/java/com/github/tomakehurst/wiremock/http/ssl/CertChainAndKey.java rename to wiremock-core/certificate-generator/src/main/java/com/github/tomakehurst/wiremock/http/ssl/CertChainAndKey.java index c839674..f089568 100644 --- c/wiremock-core/src/main/java/com/github/tomakehurst/wiremock/http/ssl/CertChainAndKey.java +++ i/wiremock-core/certificate-generator/src/main/java/com/github/tomakehurst/wiremock/http/ssl/CertChainAndKey.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2020-2025 Thomas Akehurst + * Copyright (C) 2020-2026 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 c/wiremock-core/src/main/java/com/github/tomakehurst/wiremock/http/ssl/CertificateAuthority.java i/wiremock-core/certificate-generator/src/main/java/com/github/tomakehurst/wiremock/http/ssl/CertificateAuthority.java similarity index 81% rename from wiremock-core/src/main/java/com/github/tomakehurst/wiremock/http/ssl/CertificateAuthority.java rename to wiremock-core/certificate-generator/src/main/java/com/github/tomakehurst/wiremock/http/ssl/CertificateAuthority.java index 1829fcf..b13a690 100644 --- c/wiremock-core/src/main/java/com/github/tomakehurst/wiremock/http/ssl/CertificateAuthority.java +++ i/wiremock-core/certificate-generator/src/main/java/com/github/tomakehurst/wiremock/http/ssl/CertificateAuthority.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2020-2026 Thomas Akehurst + * Copyright (C) 2026 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,12 +13,26 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package com.github.tomakehurst.wiremock.http.ssl; +package com.github.tomakehurst.wiremock.http.ssl; /* + * Copyright (C) 2020-2026 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. + */ -import static com.github.tomakehurst.wiremock.common.ArrayFunctions.prepend; import static java.util.Objects.requireNonNull; import java.io.IOException; +import java.lang.reflect.Array; import java.math.BigInteger; import java.security.KeyPair; import java.security.KeyPairGenerator; @@ -47,7 +61,6 @@ import org.bouncycastle.cert.jcajce.JcaX509v3CertificateBuilder; import org.bouncycastle.operator.ContentSigner; import org.bouncycastle.operator.OperatorCreationException; import org.bouncycastle.operator.jcajce.JcaContentSignerBuilder; -import org.jspecify.annotations.Nullable; public class CertificateAuthority { @@ -152,7 +165,7 @@ public class CertificateAuthority { Period validity, X500Principal subject, X500Principal issuer, - @nullable String sanDnsName, + String sanDnsName, boolean isCA) throws IOException, OperatorCreationException, CertificateException { ZonedDateTime start = ZonedDateTime.now().minus(Period.ofDays(1)); @@ -178,7 +191,7 @@ public class CertificateAuthority { Date notAfter, X500Principal subject, X500Principal issuer, - @nullable String sanDnsName, + String sanDnsName, boolean isCA) throws IOException, CertificateException, OperatorCreationException { SubjectPublicKeyInfo subjectKeyInfo = SubjectPublicKeyInfo.getInstance(publicKey.getEncoded()); @@ -218,4 +231,13 @@ public class CertificateAuthority { X509CertificateHolder holder = builder.build(signer); return new JcaX509CertificateConverter().getCertificate(holder); } + + public static <T> T[] prepend(T t, T[] original) { + @SuppressWarnings("unchecked") + T[] newArray = + (T[]) Array.newInstance(original.getClass().getComponentType(), original.length + 1); + newArray[0] = t; + System.arraycopy(original, 0, newArray, 1, original.length); + return newArray; + } } diff --git c/wiremock-core/src/main/java/com/github/tomakehurst/wiremock/http/ssl/CertificateGenerationUnsupportedException.java i/wiremock-core/certificate-generator/src/main/java/com/github/tomakehurst/wiremock/http/ssl/CertificateGenerationUnsupportedException.java similarity index 94% rename from wiremock-core/src/main/java/com/github/tomakehurst/wiremock/http/ssl/CertificateGenerationUnsupportedException.java rename to wiremock-core/certificate-generator/src/main/java/com/github/tomakehurst/wiremock/http/ssl/CertificateGenerationUnsupportedException.java index e7afd16..be52e96 100644 --- c/wiremock-core/src/main/java/com/github/tomakehurst/wiremock/http/ssl/CertificateGenerationUnsupportedException.java +++ i/wiremock-core/certificate-generator/src/main/java/com/github/tomakehurst/wiremock/http/ssl/CertificateGenerationUnsupportedException.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2020-2025 Thomas Akehurst + * Copyright (C) 2020-2026 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 c/wiremock-core/src/main/java/com/github/tomakehurst/wiremock/common/ArrayFunctions.java i/wiremock-core/src/main/java/com/github/tomakehurst/wiremock/common/ArrayFunctions.java index ecba478..b63da3a 100644 --- c/wiremock-core/src/main/java/com/github/tomakehurst/wiremock/common/ArrayFunctions.java +++ i/wiremock-core/src/main/java/com/github/tomakehurst/wiremock/common/ArrayFunctions.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2020-2025 Thomas Akehurst + * Copyright (C) 2020-2026 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,6 @@ package com.github.tomakehurst.wiremock.common; import static java.util.Arrays.copyOf; -import java.lang.reflect.Array; - public final class ArrayFunctions { public static <T> T[] concat(T[] first, T[] second) { @@ -30,15 +28,6 @@ public final class ArrayFunctions { return both; } - public static <T> T[] prepend(T t, T[] original) { - @SuppressWarnings("unchecked") - T[] newArray = - (T[]) Array.newInstance(original.getClass().getComponentType(), original.length + 1); - newArray[0] = t; - System.arraycopy(original, 0, newArray, 1, original.length); - return newArray; - } - private ArrayFunctions() { throw new UnsupportedOperationException("not instantiable"); } diff --git c/wiremock-standalone/build.gradle.kts i/wiremock-standalone/build.gradle.kts new file mode 100644 index 000000000..19422a1 --- /dev/null +++ i/wiremock-standalone/build.gradle.kts @@ -0,0 +1,95 @@ +plugins { + id("wiremock.common-conventions") +} + +shadow{ + addShadowVariantIntoJavaComponent = true +} + +dependencies { + implementation(project(":")) +} + +tasks.shadowJar { + archiveBaseName = "wiremock-standalone" + archiveClassifier = "" + configurations = listOf( + project.configurations.runtimeClasspath.get(), + ) + manifest { + attributes("Main-Class" to "wiremock.Run") + } + relocate("org.mortbay", "wiremock.org.mortbay") + relocate("org.eclipse", "wiremock.org.eclipse") + relocate("org.codehaus", "wiremock.org.codehaus") + relocate("com.google", "wiremock.com.google") + relocate("com.google.thirdparty", "wiremock.com.google.thirdparty") + relocate("com.fasterxml.jackson", "wiremock.com.fasterxml.jackson") + relocate("org.apache", "wiremock.org.apache") + relocate("org.xmlunit", "wiremock.org.xmlunit") + relocate("org.hamcrest", "wiremock.org.hamcrest") + relocate("org.skyscreamer", "wiremock.org.skyscreamer") + relocate("org.json", "wiremock.org.json") + relocate("net.minidev", "wiremock.net.minidev") + relocate("com.jayway", "wiremock.com.jayway") + relocate("org.objectweb", "wiremock.org.objectweb") + relocate("org.custommonkey", "wiremock.org.custommonkey") + relocate("net.javacrumbs", "wiremock.net.javacrumbs") + relocate("net.sf", "wiremock.net.sf") + relocate("com.github.jknack", "wiremock.com.github.jknack") + relocate("org.antlr", "wiremock.org.antlr") + relocate("jakarta.servlet", "wiremock.jakarta.servlet") + relocate("org.checkerframework", "wiremock.org.checkerframework") + relocate("org.hamcrest", "wiremock.org.hamcrest") + relocate("org.slf4j", "wiremock.org.slf4j") + relocate("joptsimple", "wiremock.joptsimple") + exclude("joptsimple/HelpFormatterMessages.properties") + relocate("org.yaml", "wiremock.org.yaml") + relocate("com.ethlo", "wiremock.com.ethlo") + relocate("com.networknt", "wiremock.com.networknt") + relocate("org.jspecify", "wiremock.org.jspecify") + relocate("org.bouncycastle", "wiremock.org.bouncycastle") + + dependencies { + exclude(dependency("junit:junit")) + } + + mergeServiceFiles() + + exclude("META-INF/maven/**") + exclude("META-INF/versions/17/**") + exclude("META-INF/versions/21/**") + exclude("META-INF/versions/22/**") + exclude("module-info.class") + exclude("handlebars-*.js") +} + +tasks.jar { + enabled = false +} + +publishing { + publications { + create<MavenPublication>("standaloneJar") { + artifactId = tasks.shadowJar.get().archiveBaseName.get() + from(components.findByName("shadow")) + artifact(tasks.sourcesJar) + + pom.packaging = "jar" + pom { + name = "WireMock" + description = "A web service test double for all occasions - standalone edition" + } + } + } +} + +tasks.assemble { + dependsOn(tasks.shadowJar) +} + +tasks.publish { + dependsOn( + "signStandaloneJarPublication", + ) +} diff --git c/wiremock-url/wiremock-url/src/test/resources/org/wiremock/url/whatwg/rfc3986_invalid_java_invalid.json i/wiremock-url/wiremock-url/src/test/resources/org/wiremock/url/whatwg/rfc3986_invalid_java_invalid.json index 51a6165..27aea4d 100644 --- c/wiremock-url/wiremock-url/src/test/resources/org/wiremock/url/whatwg/rfc3986_invalid_java_invalid.json +++ i/wiremock-url/wiremock-url/src/test/resources/org/wiremock/url/whatwg/rfc3986_invalid_java_invalid.json @@ -238,5 +238,9 @@ { "input": "non-special:\\/opaque", "href": "non-special:\\/opaque", "origin": "null", "protocol": "non-special:", "username": "", "password": "", "host": "", "hostname": "", "port": "", "pathname": "\\/opaque", "search": "", "hash": "" }, { "input": "non-special:/\\path", "href": "non-special:/\\path", "origin": "null", "protocol": "non-special:", "username": "", "password": "", "host": "", "hostname": "", "port": "", "pathname": "/\\path", "search": "", "hash": "" }, { "input": "non-special://host\\a", "failure": true }, -{ "input": "non-special://host/a\\b", "href": "non-special://host/a\\b", "origin": "null", "protocol": "non-special:", "username": "", "password": "", "host": "host", "hostname": "host", "port": "", "pathname": "/a\\b", "search": "", "hash": "" } +{ "input": "non-special://host/a\\b", "href": "non-special://host/a\\b", "origin": "null", "protocol": "non-special:", "username": "", "password": "", "host": "host", "hostname": "host", "port": "", "pathname": "/a\\b", "search": "", "hash": "" }, +{ "input": "data:text/plain,test#<foo> <bar>", "href": "data:text/plain,test#%3Cfoo%3E%20%3Cbar%3E", "protocol": "data:", "username": "", "password": "", "host": "", "hostname": "", "port": "", "pathname": "text/plain,test", "search": "", "hash": "#%3Cfoo%3E%20%3Cbar%3E" }, +{ "input": "about:blank#<foo> <bar>", "href": "about:blank#%3Cfoo%3E%20%3Cbar%3E", "protocol": "about:", "username": "", "password": "", "host": "", "hostname": "", "port": "", "pathname": "blank", "search": "", "hash": "#%3Cfoo%3E%20%3Cbar%3E" }, +{ "input": "data:text/plain,test#\u0000\u0001\t\n\r\u001F !\"#$%&'()*+,-./09:;<=>?@az[\\]^_`az{|}~���Éé", "href": "data:text/plain,test#%00%01%1F%20!%22#$%&'()*+,-./09:;%3C=%3E?@az[\\]^_%60az{|}~%7F%C2%80%C2%81%C3%89%C3%A9", "protocol": "data:", "username": "", "password": "", "host": "", "hostname": "", "port": "", "pathname": "text/plain,test", "search": "", "hash": "#%00%01%1F%20!%22#$%&'()*+,-./09:;%3C=%3E?@az[\\]^_%60az{|}~%7F%C2%80%C2%81%C3%89%C3%A9" }, +{ "input": "about:blank#\u0000\u0001\t\n\r\u001F !\"#$%&'()*+,-./09:;<=>?@az[\\]^_`az{|}~���Éé", "href": "about:blank#%00%01%1F%20!%22#$%&'()*+,-./09:;%3C=%3E?@az[\\]^_%60az{|}~%7F%C2%80%C2%81%C3%89%C3%A9", "protocol": "about:", "username": "", "password": "", "host": "", "hostname": "", "port": "", "pathname": "blank", "search": "", "hash": "#%00%01%1F%20!%22#$%&'()*+,-./09:;%3C=%3E?@az[\\]^_%60az{|}~%7F%C2%80%C2%81%C3%89%C3%A9" } ] \ No newline at end of file diff --git c/wiremock-url/wiremock-url/src/test/resources/org/wiremock/url/whatwg/urltestdata.json i/wiremock-url/wiremock-url/src/test/resources/org/wiremock/url/whatwg/urltestdata.json index fd2201c..ec13a88 100644 --- c/wiremock-url/wiremock-url/src/test/resources/org/wiremock/url/whatwg/urltestdata.json +++ i/wiremock-url/wiremock-url/src/test/resources/org/wiremock/url/whatwg/urltestdata.json @@ -10296,5 +10296,65 @@ "pathname": "/a\\b", "search": "", "hash": "" + }, + { + "comment": "Fragment with <> on data: URI", + "input": "data:text/plain,test#<foo> <bar>", + "base": null, + "href": "data:text/plain,test#%3Cfoo%3E%20%3Cbar%3E", + "protocol": "data:", + "username": "", + "password": "", + "host": "", + "hostname": "", + "port": "", + "pathname": "text/plain,test", + "search": "", + "hash": "#%3Cfoo%3E%20%3Cbar%3E" + }, + { + "comment": "Fragment with <> on about:blank", + "input": "about:blank#<foo> <bar>", + "base": null, + "href": "about:blank#%3Cfoo%3E%20%3Cbar%3E", + "protocol": "about:", + "username": "", + "password": "", + "host": "", + "hostname": "", + "port": "", + "pathname": "blank", + "search": "", + "hash": "#%3Cfoo%3E%20%3Cbar%3E" + }, + { + "comment": "Fragment percent-encode set on data: URI; tabs and newlines are removed", + "input":"data:text/plain,test#\u0000\u0001\t\n\r\u001f !\"#$%&'()*+,-./09:;<=>?@az[\\]^_`az{|}~\u007f\u0080\u0081Éé", + "base": null, + "href": "data:text/plain,test#%00%01%1F%20!%22#$%&'()*+,-./09:;%3C=%3E?@az[\\]^_%60az{|}~%7F%C2%80%C2%81%C3%89%C3%A9", + "protocol": "data:", + "username": "", + "password": "", + "host": "", + "hostname": "", + "port": "", + "pathname": "text/plain,test", + "search": "", + "hash": "#%00%01%1F%20!%22#$%&'()*+,-./09:;%3C=%3E?@az[\\]^_%60az{|}~%7F%C2%80%C2%81%C3%89%C3%A9" + }, + { + "comment": "Fragment percent-encode set on about:blank; tabs and newlines are removed", + "input": "about:blank#\u0000\u0001\t\n\r\u001f !\"#$%&'()*+,-./09:;<=>?@az[\\]^_`az{|}~\u007f\u0080\u0081Éé", + "base": null, + "href": "about:blank#%00%01%1F%20!%22#$%&'()*+,-./09:;%3C=%3E?@az[\\]^_%60az{|}~%7F%C2%80%C2%81%C3%89%C3%A9", + "protocol": "about:", + "username": "", + "password": "", + "host": "", + "hostname": "", + "port": "", + "pathname": "blank", + "search": "", + "hash": "#%00%01%1F%20!%22#$%&'()*+,-./09:;%3C=%3E?@az[\\]^_%60az{|}~%7F%C2%80%C2%81%C3%89%C3%A9" } ] diff --git c/wiremock-url/wiremock-url/src/test/resources/org/wiremock/url/whatwg/whatwg_valid_wiremock_invalid.json i/wiremock-url/wiremock-url/src/test/resources/org/wiremock/url/whatwg/whatwg_valid_wiremock_invalid.json index 7441abd..2264882 100644 --- c/wiremock-url/wiremock-url/src/test/resources/org/wiremock/url/whatwg/whatwg_valid_wiremock_invalid.json +++ i/wiremock-url/wiremock-url/src/test/resources/org/wiremock/url/whatwg/whatwg_valid_wiremock_invalid.json @@ -18,6 +18,8 @@ { "input": "?x", "base": "sc://ñ", "exceptionType": "IllegalUri", "exceptionMessage": "Illegal uri: `sc://ñ`", "exceptionCauseType": "IllegalAuthority", "exceptionCauseMessage": "Illegal authority: `ñ`", "source": { "input": "?x", "base": "sc://ñ", "href": "sc://%C3%B1?x", "origin": "null", "protocol": "sc:", "username": "", "password": "", "host": "%C3%B1", "hostname": "%C3%B1", "port": "", "pathname": "", "search": "?x", "hash": "" } }, { "input": "C|\n/", "base": "file://host/dir/file", "exceptionType": "IllegalUri", "exceptionMessage": "Illegal uri: `C|\n/`", "exceptionCauseType": "IllegalPath", "exceptionCauseMessage": "Illegal path: `C|\n/`", "source": { "input": "C|\n/", "base": "file://host/dir/file", "href": "file://host/C:/", "protocol": "file:", "username": "", "password": "", "host": "host", "hostname": "host", "port": "", "pathname": "/C:/", "search": "", "hash": "" } }, { "input": "[61:24:74]:98", "base": "http://example.org/foo/bar", "exceptionType": "IllegalUri", "exceptionMessage": "Illegal uri: `[61:24:74]:98`", "exceptionCauseType": "IllegalScheme", "exceptionCauseMessage": "Illegal scheme `[61`; Scheme must match [a-zA-Z][a-zA-Z0-9+\\-.]{0,255}", "source": { "input": "[61:24:74]:98", "base": "http://example.org/foo/bar", "href": "http://example.org/foo/[61:24:74]:98", "origin": "http://example.org", "protocol": "http:", "username": "", "password": "", "host": "example.org", "hostname": "example.org", "port": "", "pathname": "/foo/[61:24:74]:98", "search": "", "hash": "" } }, +{ "input": "about:blank#\u0000\u0001\t\n\r\u001F !\"#$%&'()*+,-./09:;<=>?@az[\\]^_`az{|}~���Éé", "base": null, "exceptionType": "IllegalAbsoluteUrl", "exceptionMessage": "Illegal absolute url: `about:blank#\u0000\u0001\t\n\r\u001F !\"#$%&'()*+,-./09:;<=>?@az[\\]^_`az{|}~���Éé`", "exceptionCauseType": null, "exceptionCauseMessage": null, "source": { "input": "about:blank#\u0000\u0001\t\n\r\u001F !\"#$%&'()*+,-./09:;<=>?@az[\\]^_`az{|}~���Éé", "href": "about:blank#%00%01%1F%20!%22#$%&'()*+,-./09:;%3C=%3E?@az[\\]^_%60az{|}~%7F%C2%80%C2%81%C3%89%C3%A9", "protocol": "about:", "username": "", "password": "", "host": "", "hostname": "", "port": "", "pathname": "blank", "search": "", "hash": "#%00%01%1F%20!%22#$%&'()*+,-./09:;%3C=%3E?@az[\\]^_%60az{|}~%7F%C2%80%C2%81%C3%89%C3%A9" } }, +{ "input": "data:text/plain,test#\u0000\u0001\t\n\r\u001F !\"#$%&'()*+,-./09:;<=>?@az[\\]^_`az{|}~���Éé", "base": null, "exceptionType": "IllegalAbsoluteUrl", "exceptionMessage": "Illegal absolute url: `data:text/plain,test#\u0000\u0001\t\n\r\u001F !\"#$%&'()*+,-./09:;<=>?@az[\\]^_`az{|}~���Éé`", "exceptionCauseType": null, "exceptionCauseMessage": null, "source": { "input": "data:text/plain,test#\u0000\u0001\t\n\r\u001F !\"#$%&'()*+,-./09:;<=>?@az[\\]^_`az{|}~���Éé", "href": "data:text/plain,test#%00%01%1F%20!%22#$%&'()*+,-./09:;%3C=%3E?@az[\\]^_%60az{|}~%7F%C2%80%C2%81%C3%89%C3%A9", "protocol": "data:", "username": "", "password": "", "host": "", "hostname": "", "port": "", "pathname": "text/plain,test", "search": "", "hash": "#%00%01%1F%20!%22#$%&'()*+,-./09:;%3C=%3E?@az[\\]^_%60az{|}~%7F%C2%80%C2%81%C3%89%C3%A9" } }, { "input": "file://C|/", "base": null, "exceptionType": "IllegalUri", "exceptionMessage": "Illegal uri: `file://C|/`", "exceptionCauseType": "IllegalAuthority", "exceptionCauseMessage": "Illegal authority: `C|`", "source": { "input": "file://C|/", "href": "file:///C:/", "protocol": "file:", "username": "", "password": "", "host": "", "hostname": "", "port": "", "pathname": "/C:/", "search": "", "hash": "" } }, { "input": "file://\\/localhost//cat", "base": null, "exceptionType": "IllegalUri", "exceptionMessage": "Illegal uri: `file://\\/localhost//cat`", "exceptionCauseType": "IllegalAuthority", "exceptionCauseMessage": "Illegal authority: `\\`", "source": { "input": "file://\\/localhost//cat", "href": "file:////localhost//cat", "protocol": "file:", "username": "", "password": "", "host": "", "hostname": "", "port": "", "pathname": "//localhost//cat", "search": "", "hash": "" } }, { "input": "file://ab/p", "base": null, "exceptionType": "IllegalUri", "exceptionMessage": "Illegal uri: `file://ab/p`", "exceptionCauseType": "IllegalAuthority", "exceptionCauseMessage": "Illegal authority: `ab`", "source": { "input": "file://ab/p", "href": "file://ab/p", "protocol": "file:", "username": "", "password": "", "host": "ab", "hostname": "ab", "port": "", "pathname": "/p", "search": "", "hash": "" } }, diff --git c/wiremock-url/wiremock-url/src/test/resources/org/wiremock/url/whatwg/whatwg_valid_wiremock_valid.json i/wiremock-url/wiremock-url/src/test/resources/org/wiremock/url/whatwg/whatwg_valid_wiremock_valid.json index c77cdf4..cf14c9e 100644 --- c/wiremock-url/wiremock-url/src/test/resources/org/wiremock/url/whatwg/whatwg_valid_wiremock_valid.json +++ i/wiremock-url/wiremock-url/src/test/resources/org/wiremock/url/whatwg/whatwg_valid_wiremock_valid.json @@ -1535,6 +1535,18 @@ "source" : { "input" : "about:/../", "href" : "about:/", "origin" : "null", "protocol" : "about:", "username" : "", "password" : "", "host" : "", "hostname" : "", "port" : "", "pathname" : "/", "search" : "", "hash" : "" }, "matchesWhatWg" : true }, + { + "input" : "about:blank#<foo> <bar>", + "base" : null, + "inputExpected" : { "stringValue" : "about:blank#<foo> <bar>", "type" : "OpaqueUri", "scheme" : "about", "authority" : null, "userInfo" : null, "username" : null, "password" : null, "host" : null, "port" : null, "path" : "blank", "query" : null, "fragment" : "<foo> <bar>" }, + "inputNormalised" : { "stringValue" : "about:blank#%3Cfoo%3E%20%3Cbar%3E", "type" : "OpaqueUri", "scheme" : "about", "authority" : null, "userInfo" : null, "username" : null, "password" : null, "host" : null, "port" : null, "path" : "blank", "query" : null, "fragment" : "%3Cfoo%3E%20%3Cbar%3E" }, + "baseExpected" : null, + "baseNormalised" : null, + "resolved" : { "stringValue" : "about:blank#%3Cfoo%3E%20%3Cbar%3E", "type" : "OpaqueUri", "scheme" : "about", "authority" : null, "userInfo" : null, "username" : null, "password" : null, "host" : null, "port" : null, "path" : "blank", "query" : null, "fragment" : "%3Cfoo%3E%20%3Cbar%3E" }, + "origin" : null, + "source" : { "input" : "about:blank#<foo> <bar>", "href" : "about:blank#%3Cfoo%3E%20%3Cbar%3E", "protocol" : "about:", "username" : "", "password" : "", "host" : "", "hostname" : "", "port" : "", "pathname" : "blank", "search" : "", "hash" : "#%3Cfoo%3E%20%3Cbar%3E" }, + "matchesWhatWg" : true + }, { "input" : "android-app://x:0", "base" : null, @@ -1919,6 +1931,18 @@ "source" : { "input" : "data:text/html,test#test", "base" : "http://example.org/foo/bar", "href" : "data:text/html,test#test", "origin" : "null", "protocol" : "data:", "username" : "", "password" : "", "host" : "", "hostname" : "", "port" : "", "pathname" : "text/html,test", "search" : "", "hash" : "#test" }, "matchesWhatWg" : true }, + { + "input" : "data:text/plain,test#<foo> <bar>", + "base" : null, + "inputExpected" : { "stringValue" : "data:text/plain,test#<foo> <bar>", "type" : "OpaqueUri", "scheme" : "data", "authority" : null, "userInfo" : null, "username" : null, "password" : null, "host" : null, "port" : null, "path" : "text/plain,test", "query" : null, "fragment" : "<foo> <bar>" }, + "inputNormalised" : { "stringValue" : "data:text/plain,test#%3Cfoo%3E%20%3Cbar%3E", "type" : "OpaqueUri", "scheme" : "data", "authority" : null, "userInfo" : null, "username" : null, "password" : null, "host" : null, "port" : null, "path" : "text/plain,test", "query" : null, "fragment" : "%3Cfoo%3E%20%3Cbar%3E" }, + "baseExpected" : null, + "baseNormalised" : null, + "resolved" : { "stringValue" : "data:text/plain,test#%3Cfoo%3E%20%3Cbar%3E", "type" : "OpaqueUri", "scheme" : "data", "authority" : null, "userInfo" : null, "username" : null, "password" : null, "host" : null, "port" : null, "path" : "text/plain,test", "query" : null, "fragment" : "%3Cfoo%3E%20%3Cbar%3E" }, + "origin" : null, + "source" : { "input" : "data:text/plain,test#<foo> <bar>", "href" : "data:text/plain,test#%3Cfoo%3E%20%3Cbar%3E", "protocol" : "data:", "username" : "", "password" : "", "host" : "", "hostname" : "", "port" : "", "pathname" : "text/plain,test", "search" : "", "hash" : "#%3Cfoo%3E%20%3Cbar%3E" }, + "matchesWhatWg" : true + }, { "input" : "dns://fw.example.org:9999/foo.bar.org?type=TXT", "base" : null,
bb8a369 to
5ea7e22
Compare
|
Thank you for your patience seeing this through! |
References
This pr switches the certificate generation to bouncy castle #3117 to fix issues with java 17+ and fix #3333
Closes #3117
Submitter checklist
#help-contributingor a project-specific channel like#wiremock-java