Update spotbugs and related new warnings (fixes #157)

This commit is contained in:
Richard Körber
2024-05-10 16:07:41 +02:00
parent 57ec36054a
commit aeff12088f
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