From aeff12088f496f050cdc4e44500fc335e1d5b29f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Richard=20K=C3=B6rber?= Date: Fri, 10 May 2024 16:07:41 +0200 Subject: [PATCH] Update spotbugs and related new warnings (fixes #157) --- .../java/org/shredzone/acme4j/Account.java | 2 + .../shredzone/acme4j/AcmeJsonResource.java | 2 +- .../org/shredzone/acme4j/AcmeResource.java | 5 + .../org/shredzone/acme4j/Certificate.java | 7 +- .../java/org/shredzone/acme4j/Identifier.java | 5 + .../main/java/org/shredzone/acme4j/Login.java | 3 + .../main/java/org/shredzone/acme4j/Order.java | 9 +- .../java/org/shredzone/acme4j/Session.java | 7 ++ .../acme4j/connector/HttpConnector.java | 2 + .../acme4j/connector/ResourceIterator.java | 11 +- .../exception/AcmeRateLimitedException.java | 5 +- .../provider/pebble/PebbleHttpConnector.java | 13 ++- .../provider/sslcom/SslComAcmeProvider.java | 7 +- .../org/shredzone/acme4j/toolbox/JSON.java | 107 +++++++----------- .../acme4j/smime/email/EmailProcessor.java | 7 +- .../AcmeInvalidMessageException.java | 2 + pom.xml | 4 +- src/doc/docs/migration.md | 4 + 18 files changed, 108 insertions(+), 94 deletions(-) diff --git a/acme4j-client/src/main/java/org/shredzone/acme4j/Account.java b/acme4j-client/src/main/java/org/shredzone/acme4j/Account.java index d08aab7f..d63b467e 100644 --- a/acme4j-client/src/main/java/org/shredzone/acme4j/Account.java +++ b/acme4j-client/src/main/java/org/shredzone/acme4j/Account.java @@ -24,6 +24,7 @@ import java.util.Objects; import java.util.Optional; +import edu.umd.cs.findbugs.annotations.SuppressFBWarnings; import org.shredzone.acme4j.connector.Resource; import org.shredzone.acme4j.connector.ResourceIterator; import org.shredzone.acme4j.exception.AcmeException; @@ -285,6 +286,7 @@ private EditableAccount() { * sure that they are valid according to the RFC. It is recommended to use * the {@code addContact()} methods below to add new contacts to the list. */ + @SuppressFBWarnings("EI_EXPOSE_REP") // behavior is intended public List getContacts() { return editContacts; } diff --git a/acme4j-client/src/main/java/org/shredzone/acme4j/AcmeJsonResource.java b/acme4j-client/src/main/java/org/shredzone/acme4j/AcmeJsonResource.java index 5a589034..59447b69 100644 --- a/acme4j-client/src/main/java/org/shredzone/acme4j/AcmeJsonResource.java +++ b/acme4j-client/src/main/java/org/shredzone/acme4j/AcmeJsonResource.java @@ -70,7 +70,7 @@ public JSON getJSON() { throw new AcmeLazyLoadingException(this, ex); } } - return data; + return Objects.requireNonNull(data); } /** diff --git a/acme4j-client/src/main/java/org/shredzone/acme4j/AcmeResource.java b/acme4j-client/src/main/java/org/shredzone/acme4j/AcmeResource.java index 4012a34a..dfe52969 100644 --- a/acme4j-client/src/main/java/org/shredzone/acme4j/AcmeResource.java +++ b/acme4j-client/src/main/java/org/shredzone/acme4j/AcmeResource.java @@ -87,4 +87,9 @@ public URL getLocation() { return location; } + @Override + protected final void finalize() { + // CT_CONSTRUCTOR_THROW: Prevents finalizer attack + } + } diff --git a/acme4j-client/src/main/java/org/shredzone/acme4j/Certificate.java b/acme4j-client/src/main/java/org/shredzone/acme4j/Certificate.java index 95517622..05e726f3 100644 --- a/acme4j-client/src/main/java/org/shredzone/acme4j/Certificate.java +++ b/acme4j-client/src/main/java/org/shredzone/acme4j/Certificate.java @@ -15,6 +15,7 @@ import static java.util.Collections.unmodifiableList; import static java.util.Objects.requireNonNull; +import static java.util.stream.Collectors.toList; import static java.util.stream.Collectors.toUnmodifiableList; import static org.shredzone.acme4j.toolbox.AcmeUtils.base64UrlEncode; import static org.shredzone.acme4j.toolbox.AcmeUtils.getRenewalUniqueIdentifier; @@ -33,6 +34,7 @@ import java.util.Optional; import edu.umd.cs.findbugs.annotations.Nullable; +import edu.umd.cs.findbugs.annotations.SuppressFBWarnings; import org.bouncycastle.asn1.nist.NISTObjectIdentifiers; import org.bouncycastle.asn1.x509.AlgorithmIdentifier; import org.bouncycastle.cert.X509CertificateHolder; @@ -132,9 +134,9 @@ public List getAlternateCertificates() { var login = getLogin(); alternateCerts = getAlternates().stream() .map(login::bindCertificate) - .collect(toUnmodifiableList()); + .collect(toList()); } - return alternateCerts; + return unmodifiableList(alternateCerts); } /** @@ -274,6 +276,7 @@ public boolean hasRenewalInfo() { * @throws AcmeNotSupportedException if the CA does not support renewal information. * @since 3.0.0 */ + @SuppressFBWarnings("EI_EXPOSE_REP") // behavior is intended public RenewalInfo getRenewalInfo() { if (renewalInfo == null) { renewalInfo = getRenewalInfoLocation() diff --git a/acme4j-client/src/main/java/org/shredzone/acme4j/Identifier.java b/acme4j-client/src/main/java/org/shredzone/acme4j/Identifier.java index 7c0afd5a..f973f0d6 100644 --- a/acme4j-client/src/main/java/org/shredzone/acme4j/Identifier.java +++ b/acme4j-client/src/main/java/org/shredzone/acme4j/Identifier.java @@ -256,4 +256,9 @@ public int hashCode() { return content.hashCode(); } + @Override + protected final void finalize() { + // CT_CONSTRUCTOR_THROW: Prevents finalizer attack + } + } diff --git a/acme4j-client/src/main/java/org/shredzone/acme4j/Login.java b/acme4j-client/src/main/java/org/shredzone/acme4j/Login.java index 30d4a733..3de367e8 100644 --- a/acme4j-client/src/main/java/org/shredzone/acme4j/Login.java +++ b/acme4j-client/src/main/java/org/shredzone/acme4j/Login.java @@ -22,6 +22,7 @@ import java.security.cert.X509Certificate; import java.util.Objects; +import edu.umd.cs.findbugs.annotations.SuppressFBWarnings; import org.shredzone.acme4j.challenge.Challenge; import org.shredzone.acme4j.connector.Resource; import org.shredzone.acme4j.exception.AcmeException; @@ -74,6 +75,7 @@ public Login(URL accountLocation, KeyPair keyPair, Session session) { /** * Gets the {@link Session} that is used. */ + @SuppressFBWarnings("EI_EXPOSE_REP") // behavior is intended public Session getSession() { return session; } @@ -97,6 +99,7 @@ public URL getAccountLocation() { * * @return {@link Account} bound to the login */ + @SuppressFBWarnings("EI_EXPOSE_REP") // behavior is intended public Account getAccount() { return account; } diff --git a/acme4j-client/src/main/java/org/shredzone/acme4j/Order.java b/acme4j-client/src/main/java/org/shredzone/acme4j/Order.java index 73916c73..0e79e45e 100644 --- a/acme4j-client/src/main/java/org/shredzone/acme4j/Order.java +++ b/acme4j-client/src/main/java/org/shredzone/acme4j/Order.java @@ -13,6 +13,8 @@ */ package org.shredzone.acme4j; +import static java.util.Collections.unmodifiableList; +import static java.util.stream.Collectors.toList; import static java.util.stream.Collectors.toUnmodifiableList; import java.io.IOException; @@ -25,6 +27,7 @@ import java.util.function.Consumer; import edu.umd.cs.findbugs.annotations.Nullable; +import edu.umd.cs.findbugs.annotations.SuppressFBWarnings; import org.bouncycastle.pkcs.PKCS10CertificationRequest; import org.shredzone.acme4j.exception.AcmeException; import org.shredzone.acme4j.exception.AcmeNotSupportedException; @@ -114,9 +117,9 @@ public List getAuthorizations() { .stream() .map(Value::asURL) .map(login::bindAuthorization) - .collect(toUnmodifiableList()); + .collect(toList()); } - return authorizations; + return unmodifiableList(authorizations); } /** @@ -135,6 +138,7 @@ public URL getFinalizeLocation() { * if the order is not ready yet. You must finalize the order first, and wait * for the status to become {@link Status#VALID}. */ + @SuppressFBWarnings("EI_EXPOSE_REP") // behavior is intended public Certificate getCertificate() { if (certificate == null) { certificate = getJSON().get("certificate") @@ -154,6 +158,7 @@ public Certificate getCertificate() { * for the status to become {@link Status#VALID}. It is also thrown if the * order has been {@link Status#CANCELED}. */ + @SuppressFBWarnings("EI_EXPOSE_REP") // behavior is intended public Certificate getAutoRenewalCertificate() { if (autoRenewalCertificate == null) { autoRenewalCertificate = getJSON().get("star-certificate") diff --git a/acme4j-client/src/main/java/org/shredzone/acme4j/Session.java b/acme4j-client/src/main/java/org/shredzone/acme4j/Session.java index 49a54b4e..6f4e7ccd 100644 --- a/acme4j-client/src/main/java/org/shredzone/acme4j/Session.java +++ b/acme4j-client/src/main/java/org/shredzone/acme4j/Session.java @@ -28,6 +28,7 @@ import java.util.stream.StreamSupport; import edu.umd.cs.findbugs.annotations.Nullable; +import edu.umd.cs.findbugs.annotations.SuppressFBWarnings; import org.shredzone.acme4j.connector.Connection; import org.shredzone.acme4j.connector.NetworkSettings; import org.shredzone.acme4j.connector.Resource; @@ -200,6 +201,7 @@ public String getLanguageHeader() { * @return {@link NetworkSettings} * @since 2.8 */ + @SuppressFBWarnings("EI_EXPOSE_REP") // behavior is intended public NetworkSettings networkSettings() { return networkSettings; } @@ -368,4 +370,9 @@ private void readDirectory() throws AcmeException { resourceMap.set(map); } + @Override + protected final void finalize() { + // CT_CONSTRUCTOR_THROW: Prevents finalizer attack + } + } diff --git a/acme4j-client/src/main/java/org/shredzone/acme4j/connector/HttpConnector.java b/acme4j-client/src/main/java/org/shredzone/acme4j/connector/HttpConnector.java index 8d8bfcb9..670370bd 100644 --- a/acme4j-client/src/main/java/org/shredzone/acme4j/connector/HttpConnector.java +++ b/acme4j-client/src/main/java/org/shredzone/acme4j/connector/HttpConnector.java @@ -19,6 +19,7 @@ import java.net.http.HttpRequest; import java.util.Properties; +import edu.umd.cs.findbugs.annotations.SuppressFBWarnings; import org.slf4j.LoggerFactory; /** @@ -62,6 +63,7 @@ public static String defaultUserAgent() { * Creates a new {@link HttpConnector} that is using the given * {@link NetworkSettings}. */ + @SuppressFBWarnings("EI_EXPOSE_REP2") // behavior is intended public HttpConnector(NetworkSettings networkSettings) { this.networkSettings = networkSettings; } diff --git a/acme4j-client/src/main/java/org/shredzone/acme4j/connector/ResourceIterator.java b/acme4j-client/src/main/java/org/shredzone/acme4j/connector/ResourceIterator.java index 866a6d08..581c1969 100644 --- a/acme4j-client/src/main/java/org/shredzone/acme4j/connector/ResourceIterator.java +++ b/acme4j-client/src/main/java/org/shredzone/acme4j/connector/ResourceIterator.java @@ -13,12 +13,13 @@ */ package org.shredzone.acme4j.connector; +import static java.util.Objects.requireNonNull; + import java.net.URL; import java.util.ArrayDeque; import java.util.Deque; import java.util.Iterator; import java.util.NoSuchElementException; -import java.util.Objects; import java.util.function.BiFunction; import edu.umd.cs.findbugs.annotations.Nullable; @@ -58,10 +59,10 @@ public class ResourceIterator implements Iterator { * {@link Login} and {@link URL}. */ public ResourceIterator(Login login, String field, @Nullable URL start, BiFunction creator) { - this.login = Objects.requireNonNull(login, "login"); - this.field = Objects.requireNonNull(field, "field"); + this.login = requireNonNull(login, "login"); + this.field = requireNonNull(field, "field"); this.nextUrl = start; - this.creator = Objects.requireNonNull(creator, "creator"); + this.creator = requireNonNull(creator, "creator"); } /** @@ -141,7 +142,7 @@ private void fetch() { private void readAndQueue() throws AcmeException { var session = login.getSession(); try (var conn = session.connect()) { - conn.sendSignedPostAsGetRequest(nextUrl, login); + conn.sendSignedPostAsGetRequest(requireNonNull(nextUrl), login); fillUrlList(conn.readJsonResponse()); nextUrl = conn.getLinks("next").stream().findFirst().orElse(null); diff --git a/acme4j-client/src/main/java/org/shredzone/acme4j/exception/AcmeRateLimitedException.java b/acme4j-client/src/main/java/org/shredzone/acme4j/exception/AcmeRateLimitedException.java index 9ab3a983..b1c279de 100644 --- a/acme4j-client/src/main/java/org/shredzone/acme4j/exception/AcmeRateLimitedException.java +++ b/acme4j-client/src/main/java/org/shredzone/acme4j/exception/AcmeRateLimitedException.java @@ -49,8 +49,7 @@ public AcmeRateLimitedException(Problem problem, @Nullable Instant retryAfter, @Nullable Collection documents) { super(problem); this.retryAfter = retryAfter; - this.documents = - documents != null ? Collections.unmodifiableCollection(documents) : Collections.emptyList(); + this.documents = documents != null ? documents : Collections.emptyList(); } /** @@ -66,7 +65,7 @@ public Optional getRetryAfter() { * Empty if the server did not provide such URLs. */ public Collection getDocuments() { - return documents; + return Collections.unmodifiableCollection(documents); } } diff --git a/acme4j-client/src/main/java/org/shredzone/acme4j/provider/pebble/PebbleHttpConnector.java b/acme4j-client/src/main/java/org/shredzone/acme4j/provider/pebble/PebbleHttpConnector.java index 15f2b11f..640bc486 100644 --- a/acme4j-client/src/main/java/org/shredzone/acme4j/provider/pebble/PebbleHttpConnector.java +++ b/acme4j-client/src/main/java/org/shredzone/acme4j/provider/pebble/PebbleHttpConnector.java @@ -21,11 +21,11 @@ import java.security.NoSuchAlgorithmException; import java.security.cert.CertificateException; import java.util.Objects; +import java.util.concurrent.atomic.AtomicReference; import javax.net.ssl.SSLContext; import javax.net.ssl.TrustManagerFactory; -import edu.umd.cs.findbugs.annotations.Nullable; import org.shredzone.acme4j.connector.HttpConnector; import org.shredzone.acme4j.connector.NetworkSettings; @@ -34,7 +34,7 @@ * certificate. */ public class PebbleHttpConnector extends HttpConnector { - private static @Nullable SSLContext sslContext = null; + private static final AtomicReference SSL_CONTEXT_REF = new AtomicReference<>(); public PebbleHttpConnector(NetworkSettings settings) { super(settings); @@ -51,8 +51,8 @@ public HttpClient.Builder createClientBuilder() { * Lazily creates an {@link SSLContext} that exclusively accepts the Pebble * certificate. */ - protected synchronized SSLContext createSSLContext() { - if (sslContext == null) { + protected SSLContext createSSLContext() { + if (SSL_CONTEXT_REF.get() == null) { try (var in = getClass().getResourceAsStream("/org/shredzone/acme4j/provider/pebble/pebble.truststore")) { var keystore = KeyStore.getInstance(KeyStore.getDefaultType()); keystore.load(in, "acme4j".toCharArray()); @@ -60,14 +60,15 @@ protected synchronized SSLContext createSSLContext() { var tmf = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm()); tmf.init(keystore); - sslContext = SSLContext.getInstance("TLS"); + var sslContext = SSLContext.getInstance("TLS"); sslContext.init(null, tmf.getTrustManagers(), null); + SSL_CONTEXT_REF.set(sslContext); } catch (IOException | KeyStoreException | CertificateException | NoSuchAlgorithmException | KeyManagementException ex) { throw new RuntimeException("Could not create truststore", ex); } } - return Objects.requireNonNull(sslContext); + return Objects.requireNonNull(SSL_CONTEXT_REF.get()); } } diff --git a/acme4j-client/src/main/java/org/shredzone/acme4j/provider/sslcom/SslComAcmeProvider.java b/acme4j-client/src/main/java/org/shredzone/acme4j/provider/sslcom/SslComAcmeProvider.java index b7f02a61..fcc8ba8f 100644 --- a/acme4j-client/src/main/java/org/shredzone/acme4j/provider/sslcom/SslComAcmeProvider.java +++ b/acme4j-client/src/main/java/org/shredzone/acme4j/provider/sslcom/SslComAcmeProvider.java @@ -79,7 +79,12 @@ public JSON directory(Session session, URI serverUri) throws AcmeException { // by EAB, but the "externalAccountRequired" flag in the directory is set to // false. This patch reads the directory and forcefully sets the flag to true. // The entire method can be removed once it is fixed on SSL.com side. - var directory = super.directory(session, serverUri).toMap(); + var superdirectory = super.directory(session, serverUri); + if (superdirectory == null) { + return null; + } + + var directory = superdirectory.toMap(); var meta = directory.get("meta"); if (meta instanceof Map) { var metaMap = ((Map) meta); diff --git a/acme4j-client/src/main/java/org/shredzone/acme4j/toolbox/JSON.java b/acme4j-client/src/main/java/org/shredzone/acme4j/toolbox/JSON.java index 6f06baba..f52abfbc 100644 --- a/acme4j-client/src/main/java/org/shredzone/acme4j/toolbox/JSON.java +++ b/acme4j-client/src/main/java/org/shredzone/acme4j/toolbox/JSON.java @@ -21,8 +21,6 @@ import java.io.IOException; import java.io.InputStream; import java.io.InputStreamReader; -import java.io.ObjectInputStream; -import java.io.ObjectOutputStream; import java.io.Serializable; import java.net.MalformedURLException; import java.net.URI; @@ -44,7 +42,6 @@ import java.util.stream.StreamSupport; import edu.umd.cs.findbugs.annotations.Nullable; -import edu.umd.cs.findbugs.annotations.SuppressFBWarnings; import org.jose4j.json.JsonUtil; import org.jose4j.lang.JoseException; import org.shredzone.acme4j.Identifier; @@ -57,14 +54,12 @@ * A model containing a JSON result. The content is immutable. */ public final class JSON implements Serializable { - private static final long serialVersionUID = 3091273044605709204L; + private static final long serialVersionUID = 418332625174149030L; private static final JSON EMPTY_JSON = new JSON(new HashMap<>()); private final String path; - - @SuppressFBWarnings("JCIP_FIELD_ISNT_FINAL_IN_IMMUTABLE_CLASS") - private transient Map data; // Must not be final for deserialization + private final Map data; /** * Creates a new {@link JSON} root object. @@ -207,26 +202,6 @@ public Map toMap() { return Collections.unmodifiableMap(data); } - /** - * Serialize the data map in JSON. - */ - private void writeObject(ObjectOutputStream out) throws IOException { - out.writeUTF(JsonUtil.toJson(data)); - out.defaultWriteObject(); - } - - /** - * Deserialize the JSON representation of the data map. - */ - private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException { - try { - data = new HashMap<>(JsonUtil.parseJson(in.readUTF())); - in.defaultReadObject(); - } catch (JoseException ex) { - throw new AcmeProtocolException("Cannot deserialize", ex); - } - } - /** * Represents a JSON array. */ @@ -369,20 +344,15 @@ public Optional map(Function mapper) { * Returns the value as {@link String}. */ public String asString() { - required(); - return val.toString(); + return required().toString(); } /** * Returns the value as JSON object. */ + @SuppressWarnings("unchecked") public JSON asObject() { - required(); - try { - return new JSON(path, (Map) val); - } catch (ClassCastException ex) { - throw new AcmeProtocolException(path + ": expected an object", ex); - } + return new JSON(path, (Map) required(Map.class)); } /** @@ -391,9 +361,8 @@ public JSON asObject() { * @since 2.8 */ public JSON asEncodedObject() { - required(); try { - var raw = AcmeUtils.base64UrlDecode(val.toString()); + var raw = AcmeUtils.base64UrlDecode(asString()); return new JSON(path, JsonUtil.parseJson(new String(raw, UTF_8))); } catch (IllegalArgumentException | JoseException ex) { throw new AcmeProtocolException(path + ": expected an encoded object", ex); @@ -407,7 +376,6 @@ public JSON asEncodedObject() { * Base {@link URL} to resolve relative links against */ public Problem asProblem(URL baseUrl) { - required(); return new Problem(asObject(), baseUrl); } @@ -417,7 +385,6 @@ public Problem asProblem(URL baseUrl) { * @since 2.3 */ public Identifier asIdentifier() { - required(); return new Identifier(asObject()); } @@ -427,6 +394,7 @@ public Identifier asIdentifier() { * Unlike the other getters, this method returns an empty array if the value is * not set. Use {@link #isPresent()} to find out if the value was actually set. */ + @SuppressWarnings("unchecked") public Array asArray() { if (val == null) { return new Array(path, Collections.emptyList()); @@ -443,33 +411,22 @@ public Array asArray() { * Returns the value as int. */ public int asInt() { - required(); - try { - return ((Number) val).intValue(); - } catch (ClassCastException ex) { - throw new AcmeProtocolException(path + ": bad number " + val, ex); - } + return (required(Number.class)).intValue(); } /** * Returns the value as boolean. */ public boolean asBoolean() { - required(); - try { - return (Boolean) val; - } catch (ClassCastException ex) { - throw new AcmeProtocolException(path + ": bad boolean " + val, ex); - } + return required(Boolean.class); } /** * Returns the value as {@link URI}. */ public URI asURI() { - required(); try { - return new URI(val.toString()); + return new URI(asString()); } catch (URISyntaxException ex) { throw new AcmeProtocolException(path + ": bad URI " + val, ex); } @@ -479,9 +436,8 @@ public URI asURI() { * Returns the value as {@link URL}. */ public URL asURL() { - required(); try { - return new URL(val.toString()); + return new URL(asString()); } catch (MalformedURLException ex) { throw new AcmeProtocolException(path + ": bad URL " + val, ex); } @@ -491,9 +447,8 @@ public URL asURL() { * Returns the value as {@link Instant}. */ public Instant asInstant() { - required(); try { - return parseTimestamp(val.toString()); + return parseTimestamp(asString()); } catch (IllegalArgumentException ex) { throw new AcmeProtocolException(path + ": bad date " + val, ex); } @@ -505,38 +460,52 @@ public Instant asInstant() { * @since 2.3 */ public Duration asDuration() { - required(); - try { - return Duration.ofSeconds(((Number) val).longValue()); - } catch (ClassCastException ex) { - throw new AcmeProtocolException(path + ": bad duration " + val, ex); - } + return Duration.ofSeconds(required(Number.class).longValue()); } /** * Returns the value as base64 decoded byte array. */ public byte[] asBinary() { - required(); - return AcmeUtils.base64UrlDecode(val.toString()); + return AcmeUtils.base64UrlDecode(asString()); } /** * Returns the parsed {@link Status}. */ public Status asStatus() { - required(); - return Status.parse(val.toString()); + return Status.parse(asString()); } /** * Checks if the value is present. An {@link AcmeProtocolException} is thrown if * the value is {@code null}. + * + * @return val that is guaranteed to be non-{@code null} + */ + private Object required() { + if (val == null) { + throw new AcmeProtocolException(path + ": required, but not set"); + } + return val; + } + + /** + * Checks if the value is present. An {@link AcmeProtocolException} is thrown if + * the value is {@code null} or is not of the expected type. + * + * @param type + * expected type + * @return val that is guaranteed to be non-{@code null} */ - private void required() { - if (!isPresent()) { + private T required(Class type) { + if (val == null) { throw new AcmeProtocolException(path + ": required, but not set"); } + if (!type.isInstance(val)) { + throw new AcmeProtocolException(path + ": cannot convert to " + type.getSimpleName()); + } + return type.cast(val); } @Override diff --git a/acme4j-smime/src/main/java/org/shredzone/acme4j/smime/email/EmailProcessor.java b/acme4j-smime/src/main/java/org/shredzone/acme4j/smime/email/EmailProcessor.java index c7f11a74..2a6a1900 100644 --- a/acme4j-smime/src/main/java/org/shredzone/acme4j/smime/email/EmailProcessor.java +++ b/acme4j-smime/src/main/java/org/shredzone/acme4j/smime/email/EmailProcessor.java @@ -13,6 +13,7 @@ */ package org.shredzone.acme4j.smime.email; +import static java.util.Collections.unmodifiableCollection; import static java.util.Objects.requireNonNull; import java.net.URL; @@ -208,14 +209,14 @@ public EmailProcessor expectedIdentifier(Identifier expectedIdentifier) { * Returns the sender of the "challenge" email. */ public InternetAddress getSender() { - return sender; + return (InternetAddress) sender.clone(); } /** * Returns the recipient of the "challenge" email. */ public InternetAddress getRecipient() { - return recipient; + return (InternetAddress) recipient.clone(); } /** @@ -224,7 +225,7 @@ public InternetAddress getRecipient() { * Empty if there was no reply-to header, but never {@code null}. */ public Collection getReplyTo() { - return replyTo; + return unmodifiableCollection(replyTo); } /** diff --git a/acme4j-smime/src/main/java/org/shredzone/acme4j/smime/exception/AcmeInvalidMessageException.java b/acme4j-smime/src/main/java/org/shredzone/acme4j/smime/exception/AcmeInvalidMessageException.java index 843a6659..a2aeddaf 100644 --- a/acme4j-smime/src/main/java/org/shredzone/acme4j/smime/exception/AcmeInvalidMessageException.java +++ b/acme4j-smime/src/main/java/org/shredzone/acme4j/smime/exception/AcmeInvalidMessageException.java @@ -20,6 +20,7 @@ import java.util.List; import java.util.Optional; +import edu.umd.cs.findbugs.annotations.SuppressFBWarnings; import org.bouncycastle.i18n.ErrorBundle; import org.bouncycastle.i18n.LocalizedException; import org.shredzone.acme4j.exception.AcmeException; @@ -93,6 +94,7 @@ public AcmeInvalidMessageException(String msg, Throwable cause) { * * @since 2.16 */ + @SuppressFBWarnings("EI_EXPOSE_REP") // errors is always an unmodifiable list public List getErrors() { return errors; } diff --git a/pom.xml b/pom.xml index f578ae8c..15643e57 100644 --- a/pom.xml +++ b/pom.xml @@ -80,7 +80,7 @@ com.github.spotbugs spotbugs-maven-plugin - 4.2.3 + 4.8.5.0 @@ -177,7 +177,7 @@ com.github.spotbugs spotbugs-annotations - 4.8.3 + 4.8.5 true diff --git a/src/doc/docs/migration.md b/src/doc/docs/migration.md index 38d40567..5ff19298 100644 --- a/src/doc/docs/migration.md +++ b/src/doc/docs/migration.md @@ -2,6 +2,10 @@ This document will help you migrate your code to the latest _acme4j_ version. +## Migration to Version 3.2.2 + +- This version is unable to deserialize resource objects that were serialized by a previous version using Java's serialization mechanism. This shouldn't be a problem, as [it was not allowed](usage/persistence.md#serialization) to share serialized data between different versions anyway. + ## Migration to Version 3.2.0 - Starting with this version, the `CSRBuilder` won't add the first domain as common name automatically. This permits the issuance of very long domain names, and should have no negative impact otherwise, as this field is usually ignored by CAs anyway. If you should encounter a problem here, you can use `CSRBuilder.setCommonName()` to set the first domain as common name manually. Discussion see [here](https://community.letsencrypt.org/t/questions-re-simplifying-issuance-for-very-long-domain-names/207925/11).