Update spotbugs and related new warnings (fixes #157)

pull/168/head
Richard Körber 2024-05-10 16:07:41 +02:00
parent 57ec36054a
commit aeff12088f
No known key found for this signature in database
GPG Key ID: AAB9FD19C78AA3E0
18 changed files with 108 additions and 94 deletions

View File

@ -24,6 +24,7 @@ import java.util.List;
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 @@ public class Account extends AcmeJsonResource {
* 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<URI> getContacts() {
return editContacts;
}

View File

@ -70,7 +70,7 @@ public abstract class AcmeJsonResource extends AcmeResource {
throw new AcmeLazyLoadingException(this, ex);
}
}
return data;
return Objects.requireNonNull(data);
}
/**

View File

@ -87,4 +87,9 @@ public abstract class AcmeResource implements Serializable {
return location;
}
@Override
protected final void finalize() {
// CT_CONSTRUCTOR_THROW: Prevents finalizer attack
}
}

View File

@ -15,6 +15,7 @@ package org.shredzone.acme4j;
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.List;
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 class Certificate extends AcmeResource {
var login = getLogin();
alternateCerts = getAlternates().stream()
.map(login::bindCertificate)
.collect(toUnmodifiableList());
.collect(toList());
}
return alternateCerts;
return unmodifiableList(alternateCerts);
}
/**
@ -274,6 +276,7 @@ public class Certificate extends AcmeResource {
* @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()

View File

@ -256,4 +256,9 @@ public class Identifier implements Serializable {
return content.hashCode();
}
@Override
protected final void finalize() {
// CT_CONSTRUCTOR_THROW: Prevents finalizer attack
}
}

View File

@ -22,6 +22,7 @@ import java.security.KeyPair;
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 class Login {
/**
* Gets the {@link Session} that is used.
*/
@SuppressFBWarnings("EI_EXPOSE_REP") // behavior is intended
public Session getSession() {
return session;
}
@ -97,6 +99,7 @@ public class Login {
*
* @return {@link Account} bound to the login
*/
@SuppressFBWarnings("EI_EXPOSE_REP") // behavior is intended
public Account getAccount() {
return account;
}

View File

@ -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.Optional;
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 class Order extends AcmeJsonResource {
.stream()
.map(Value::asURL)
.map(login::bindAuthorization)
.collect(toUnmodifiableList());
.collect(toList());
}
return authorizations;
return unmodifiableList(authorizations);
}
/**
@ -135,6 +138,7 @@ public class Order extends AcmeJsonResource {
* 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 class Order extends AcmeJsonResource {
* 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")

View File

@ -28,6 +28,7 @@ import java.util.concurrent.atomic.AtomicReference;
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 class Session {
* @return {@link NetworkSettings}
* @since 2.8
*/
@SuppressFBWarnings("EI_EXPOSE_REP") // behavior is intended
public NetworkSettings networkSettings() {
return networkSettings;
}
@ -368,4 +370,9 @@ public class Session {
resourceMap.set(map);
}
@Override
protected final void finalize() {
// CT_CONSTRUCTOR_THROW: Prevents finalizer attack
}
}

View File

@ -19,6 +19,7 @@ import java.net.http.HttpClient;
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 class HttpConnector {
* 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;
}

View File

@ -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<T extends AcmeResource> implements Iterator<T> {
* {@link Login} and {@link URL}.
*/
public ResourceIterator(Login login, String field, @Nullable URL start, BiFunction<Login, URL, T> 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 @@ public class ResourceIterator<T extends AcmeResource> implements Iterator<T> {
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);

View File

@ -49,8 +49,7 @@ public class AcmeRateLimitedException extends AcmeServerException {
@Nullable Collection<URL> 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 class AcmeRateLimitedException extends AcmeServerException {
* Empty if the server did not provide such URLs.
*/
public Collection<URL> getDocuments() {
return documents;
return Collections.unmodifiableCollection(documents);
}
}

View File

@ -21,11 +21,11 @@ import java.security.KeyStoreException;
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 @@ import org.shredzone.acme4j.connector.NetworkSettings;
* certificate.
*/
public class PebbleHttpConnector extends HttpConnector {
private static @Nullable SSLContext sslContext = null;
private static final AtomicReference<SSLContext> SSL_CONTEXT_REF = new AtomicReference<>();
public PebbleHttpConnector(NetworkSettings settings) {
super(settings);
@ -51,8 +51,8 @@ public class PebbleHttpConnector extends HttpConnector {
* 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 @@ public class PebbleHttpConnector extends HttpConnector {
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());
}
}

View File

@ -79,7 +79,12 @@ public class SslComAcmeProvider extends AbstractAcmeProvider {
// 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<String, Object>) meta);

View File

@ -21,8 +21,6 @@ import java.io.BufferedReader;
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.Stream;
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 @@ import org.shredzone.acme4j.exception.AcmeProtocolException;
* 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<String, Object> data; // Must not be final for deserialization
private final Map<String, Object> data;
/**
* Creates a new {@link JSON} root object.
@ -207,26 +202,6 @@ public final class JSON implements Serializable {
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 final class JSON implements Serializable {
* 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<String, Object>) val);
} catch (ClassCastException ex) {
throw new AcmeProtocolException(path + ": expected an object", ex);
}
return new JSON(path, (Map<String, Object>) required(Map.class));
}
/**
@ -391,9 +361,8 @@ public final class JSON implements Serializable {
* @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 final class JSON implements Serializable {
* Base {@link URL} to resolve relative links against
*/
public Problem asProblem(URL baseUrl) {
required();
return new Problem(asObject(), baseUrl);
}
@ -417,7 +385,6 @@ public final class JSON implements Serializable {
* @since 2.3
*/
public Identifier asIdentifier() {
required();
return new Identifier(asObject());
}
@ -427,6 +394,7 @@ public final class JSON implements Serializable {
* 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 final class JSON implements Serializable {
* 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 final class JSON implements Serializable {
* 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 final class JSON implements Serializable {
* 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 final class JSON implements Serializable {
* @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 void required() {
if (!isPresent()) {
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 <T> T required(Class<T> 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

View File

@ -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 final class EmailProcessor {
* 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 final class EmailProcessor {
* Empty if there was no reply-to header, but never {@code null}.
*/
public Collection<InternetAddress> getReplyTo() {
return replyTo;
return unmodifiableCollection(replyTo);
}
/**

View File

@ -20,6 +20,7 @@ import java.util.Collections;
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 class AcmeInvalidMessageException extends AcmeException {
*
* @since 2.16
*/
@SuppressFBWarnings("EI_EXPOSE_REP") // errors is always an unmodifiable list
public List<ErrorBundle> getErrors() {
return errors;
}

View File

@ -80,7 +80,7 @@
<plugin>
<groupId>com.github.spotbugs</groupId>
<artifactId>spotbugs-maven-plugin</artifactId>
<version>4.2.3</version>
<version>4.8.5.0</version>
<executions>
<execution>
<goals>
@ -177,7 +177,7 @@
<dependency>
<groupId>com.github.spotbugs</groupId>
<artifactId>spotbugs-annotations</artifactId>
<version>4.8.3</version>
<version>4.8.5</version>
<optional>true</optional>
<exclusions>
<exclusion>

View File

@ -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).