diff --git a/acme4j-client/src/main/java/org/shredzone/acme4j/Authorization.java b/acme4j-client/src/main/java/org/shredzone/acme4j/Authorization.java index 68266d8d..da74194b 100644 --- a/acme4j-client/src/main/java/org/shredzone/acme4j/Authorization.java +++ b/acme4j-client/src/main/java/org/shredzone/acme4j/Authorization.java @@ -47,23 +47,25 @@ public class Authorization extends AcmeResource { private List> combinations; private boolean loaded = false; + protected Authorization(Session session, URI location) { + super(session); + setLocation(location); + } + /** - * Creates a new instance of {@link Authorization} and binds it to the {@link Session}. + * Creates a new instance of {@link Authorization} and binds it to the + * {@link Session}. * * @param session * {@link Session} to be used * @param location * Location of the Authorization + * @return {@link Authorization} bound to the session and location */ public static Authorization bind(Session session, URI location) { return new Authorization(session, location); } - protected Authorization(Session session, URI location) { - super(session); - setLocation(location); - } - /** * Gets the domain name to be authorized. */ @@ -240,6 +242,20 @@ public class Authorization extends AcmeResource { domain = jsonIdentifier.get("value").asString(); } + challenges = fetchChallenges(json); + combinations = fetchCombinations(json, challenges); + + loaded = true; + } + + /** + * Fetches all {@link Challenge} that are defined in the JSON. + * + * @param json + * {@link JSON} to read + * @return List of {@link Challenge} + */ + private List fetchChallenges(JSON json) { JSON.Array jsonChallenges = json.get("challenges").asArray(); List cr = new ArrayList<>(); for (JSON.Value c : jsonChallenges) { @@ -248,28 +264,34 @@ public class Authorization extends AcmeResource { cr.add(ch); } } - challenges = cr; + return cr; + } + /** + * Fetches all possible combination of {@link Challenge} that are defined in the JSON. + * + * @param json + * {@link JSON} to read + * @param challenges + * List of available {@link Challenge} + * @return List of {@link Challenge} combinations + */ + private List> fetchCombinations(JSON json, List challenges) { JSON.Array jsonCombinations = json.get("combinations").asArray(); - if (jsonCombinations != null) { - List> cmb = new ArrayList<>(jsonCombinations.size()); - - for (int ix = 0; ix < jsonCombinations.size(); ix++) { - JSON.Array c = jsonCombinations.get(ix).asArray(); - List clist = new ArrayList<>(c.size()); - for (JSON.Value n : c) { - clist.add(cr.get(n.asInt())); - } - cmb.add(clist); - } - combinations = cmb; - } else { - List> cmb = new ArrayList<>(1); - cmb.add(cr); - combinations = cmb; + if (jsonCombinations == null) { + return Arrays.asList(challenges); } - loaded = true; + List> cmb = new ArrayList<>(jsonCombinations.size()); + for (JSON.Value v : jsonCombinations) { + JSON.Array c = v.asArray(); + List clist = new ArrayList<>(c.size()); + for (JSON.Value n : c) { + clist.add(challenges.get(n.asInt())); + } + cmb.add(clist); + } + return cmb; } } 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 6e5bf616..ea4b7510 100644 --- a/acme4j-client/src/main/java/org/shredzone/acme4j/Certificate.java +++ b/acme4j-client/src/main/java/org/shredzone/acme4j/Certificate.java @@ -41,18 +41,6 @@ public class Certificate extends AcmeResource { private X509Certificate cert = null; private X509Certificate[] chain = null; - /** - * Creates a new instance of {@link Certificate} and binds it to the {@link Session}. - * - * @param session - * {@link Session} to be used - * @param location - * Location of the Certificate - */ - public static Certificate bind(Session session, URI location) { - return new Certificate(session, location); - } - protected Certificate(Session session, URI certUri) { super(session); setLocation(certUri); @@ -65,6 +53,19 @@ public class Certificate extends AcmeResource { this.cert = cert; } + /** + * Creates a new instance of {@link Certificate} and binds it to the {@link Session}. + * + * @param session + * {@link Session} to be used + * @param location + * Location of the Certificate + * @return {@link Certificate} bound to the session and location + */ + public static Certificate bind(Session session, URI location) { + return new Certificate(session, location); + } + /** * Returns the URI of the certificate chain. {@code null} if not known or not * available. diff --git a/acme4j-client/src/main/java/org/shredzone/acme4j/Metadata.java b/acme4j-client/src/main/java/org/shredzone/acme4j/Metadata.java index acb485fc..0dd83e95 100644 --- a/acme4j-client/src/main/java/org/shredzone/acme4j/Metadata.java +++ b/acme4j-client/src/main/java/org/shredzone/acme4j/Metadata.java @@ -16,6 +16,7 @@ package org.shredzone.acme4j; import java.net.URI; import java.util.ArrayList; import java.util.Collection; +import java.util.Collections; import java.util.List; import org.shredzone.acme4j.util.JSON; @@ -57,12 +58,12 @@ public class Metadata { /** * Returns a collection of hostnames, which the ACME server recognises as referring to - * itself for the purposes of CAA record validation. {@code null} if not available. + * itself for the purposes of CAA record validation. Empty if not available. */ public Collection getCaaIdentities() { Array array = meta.get("caa-identities").asArray(); if (array == null) { - return null; + return Collections.emptyList(); } List result = new ArrayList<>(array.size()); diff --git a/acme4j-client/src/main/java/org/shredzone/acme4j/Registration.java b/acme4j-client/src/main/java/org/shredzone/acme4j/Registration.java index 20d6b30b..e873b4c6 100644 --- a/acme4j-client/src/main/java/org/shredzone/acme4j/Registration.java +++ b/acme4j-client/src/main/java/org/shredzone/acme4j/Registration.java @@ -48,6 +48,12 @@ public class Registration extends AcmeResource { private static final long serialVersionUID = -8177333806740391140L; private static final Logger LOG = LoggerFactory.getLogger(Registration.class); + private static final String KEY_AGREEMENT = "agreement"; + private static final String KEY_AUTHORIZATIONS = "authorizations"; + private static final String KEY_CERTIFICATES = "certificates"; + private static final String KEY_CONTACT = "contact"; + private static final String KEY_STATUS = "status"; + private final List contacts = new ArrayList<>(); private URI agreement; private URI authorizations; @@ -55,18 +61,6 @@ public class Registration extends AcmeResource { private Status status; private boolean loaded = false; - /** - * Creates a new instance of {@link Registration} and binds it to the {@link Session}. - * - * @param session - * {@link Session} to be used - * @param location - * Location URI of the registration - */ - public static Registration bind(Session session, URI location) { - return new Registration(session, location); - } - protected Registration(Session session, URI location) { super(session); setLocation(location); @@ -78,6 +72,19 @@ public class Registration extends AcmeResource { this.agreement = agreement; } + /** + * Creates a new instance of {@link Registration} and binds it to the {@link Session}. + * + * @param session + * {@link Session} to be used + * @param location + * Location URI of the registration + * @return {@link Registration} bound to the session and location + */ + public static Registration bind(Session session, URI location) { + return new Registration(session, location); + } + /** * Returns the URI of the agreement document the user is required to accept. */ @@ -118,7 +125,7 @@ public class Registration extends AcmeResource { public Iterator getAuthorizations() throws AcmeException { LOG.debug("getAuthorizations"); load(); - return new ResourceIterator(getSession(), "authorizations", authorizations) { + return new ResourceIterator(getSession(), KEY_AUTHORIZATIONS, authorizations) { @Override protected Authorization create(Session session, URI uri) { return Authorization.bind(session, uri); @@ -140,7 +147,7 @@ public class Registration extends AcmeResource { public Iterator getCertificates() throws AcmeException { LOG.debug("getCertificates"); load(); - return new ResourceIterator(getSession(), "certificates", certificates) { + return new ResourceIterator(getSession(), KEY_CERTIFICATES, certificates) { @Override protected Certificate create(Session session, URI uri) { return Certificate.bind(session, uri); @@ -317,7 +324,7 @@ public class Registration extends AcmeResource { try (Connection conn = getSession().provider().connect()) { JSONBuilder claims = new JSONBuilder(); claims.putResource("reg"); - claims.put("status", "deactivated"); + claims.put(KEY_STATUS, "deactivated"); conn.sendSignedRequest(getLocation(), claims, getSession()); conn.accept(HttpURLConnection.HTTP_OK, HttpURLConnection.HTTP_ACCEPTED); @@ -349,22 +356,22 @@ public class Registration extends AcmeResource { * {@link Connection} with headers to be evaluated */ private void unmarshal(JSON json, Connection conn) { - if (json.contains("agreement")) { - this.agreement = json.get("agreement").asURI(); + if (json.contains(KEY_AGREEMENT)) { + this.agreement = json.get(KEY_AGREEMENT).asURI(); } - if (json.contains("contact")) { + if (json.contains(KEY_CONTACT)) { contacts.clear(); - for (JSON.Value v : json.get("contact").asArray()) { + for (JSON.Value v : json.get(KEY_CONTACT).asArray()) { contacts.add(v.asURI()); } } - this.authorizations = json.get("authorizations").asURI(); - this.certificates = json.get("certificates").asURI(); + this.authorizations = json.get(KEY_AUTHORIZATIONS).asURI(); + this.certificates = json.get(KEY_CERTIFICATES).asURI(); - if (json.contains("status")) { - this.status = Status.parse(json.get("status").asString()); + if (json.contains(KEY_STATUS)) { + this.status = Status.parse(json.get(KEY_STATUS).asString()); } URI location = conn.getLocation(); @@ -396,7 +403,7 @@ public class Registration extends AcmeResource { private final List editContacts = new ArrayList<>(); private URI editAgreement; - public EditableRegistration() { + private EditableRegistration() { editContacts.addAll(Registration.this.contacts); editAgreement = Registration.this.agreement; } @@ -414,6 +421,7 @@ public class Registration extends AcmeResource { * * @param contact * Contact URI + * @return itself */ public EditableRegistration addContact(URI contact) { editContacts.add(contact); @@ -427,6 +435,7 @@ public class Registration extends AcmeResource { * * @param contact * Contact URI as string + * @return itself */ public EditableRegistration addContact(String contact) { addContact(URI.create(contact)); @@ -438,6 +447,7 @@ public class Registration extends AcmeResource { * * @param agreement * New agreement URI + * @return itself */ public EditableRegistration setAgreement(URI agreement) { this.editAgreement = agreement; @@ -453,10 +463,10 @@ public class Registration extends AcmeResource { JSONBuilder claims = new JSONBuilder(); claims.putResource("reg"); if (!editContacts.isEmpty()) { - claims.put("contact", editContacts); + claims.put(KEY_CONTACT, editContacts); } if (editAgreement != null) { - claims.put("agreement", editAgreement); + claims.put(KEY_AGREEMENT, editAgreement); } conn.sendSignedRequest(getLocation(), claims, getSession()); diff --git a/acme4j-client/src/main/java/org/shredzone/acme4j/RegistrationBuilder.java b/acme4j-client/src/main/java/org/shredzone/acme4j/RegistrationBuilder.java index 116d61fa..1acf1171 100644 --- a/acme4j-client/src/main/java/org/shredzone/acme4j/RegistrationBuilder.java +++ b/acme4j-client/src/main/java/org/shredzone/acme4j/RegistrationBuilder.java @@ -39,6 +39,7 @@ public class RegistrationBuilder { * * @param contact * Contact URI + * @return itself */ public RegistrationBuilder addContact(URI contact) { contacts.add(contact); @@ -54,6 +55,7 @@ public class RegistrationBuilder { * Contact URI as string * @throws IllegalArgumentException * if there is a syntax error in the URI string + * @return itself */ public RegistrationBuilder addContact(String contact) { addContact(URI.create(contact)); diff --git a/acme4j-client/src/main/java/org/shredzone/acme4j/RevocationReason.java b/acme4j-client/src/main/java/org/shredzone/acme4j/RevocationReason.java index 1935369c..52ddaf36 100644 --- a/acme4j-client/src/main/java/org/shredzone/acme4j/RevocationReason.java +++ b/acme4j-client/src/main/java/org/shredzone/acme4j/RevocationReason.java @@ -32,6 +32,19 @@ public enum RevocationReason { PRIVILEGE_WITHDRAWN(9), AA_COMPROMISE(10); + private final int reasonCode; + + private RevocationReason(int reasonCode) { + this.reasonCode = reasonCode; + } + + /** + * Returns the reason code as defined in RFC 5280. + */ + public int getReasonCode() { + return reasonCode; + } + /** * Returns the {@link RevocationReason} that matches the reason code. * @@ -48,17 +61,4 @@ public enum RevocationReason { return null; } - private final int reasonCode; - - private RevocationReason(int reasonCode) { - this.reasonCode = reasonCode; - } - - /** - * Returns the reason code as defined in RFC 5280. - */ - public int getReasonCode() { - return reasonCode; - } - } 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 e524b2e2..a6131bdf 100644 --- a/acme4j-client/src/main/java/org/shredzone/acme4j/Session.java +++ b/acme4j-client/src/main/java/org/shredzone/acme4j/Session.java @@ -129,6 +129,8 @@ public class Session { * Returns the {@link AcmeProvider} that is used for this session. *

* The {@link AcmeProvider} instance is lazily created and cached. + * + * @return {@link AcmeProvider} */ public AcmeProvider provider() { synchronized (this) { diff --git a/acme4j-client/src/main/java/org/shredzone/acme4j/challenge/Challenge.java b/acme4j-client/src/main/java/org/shredzone/acme4j/challenge/Challenge.java index bdc003b7..e062ccf8 100644 --- a/acme4j-client/src/main/java/org/shredzone/acme4j/challenge/Challenge.java +++ b/acme4j-client/src/main/java/org/shredzone/acme4j/challenge/Challenge.java @@ -50,6 +50,16 @@ public class Challenge extends AcmeResource { private JSON data = JSON.empty(); + /** + * Creates a new generic {@link Challenge} object. + * + * @param session + * {@link Session} to bind to. + */ + public Challenge(Session session) { + super(session); + } + /** * Returns a {@link Challenge} object of an existing challenge. * @@ -57,7 +67,7 @@ public class Challenge extends AcmeResource { * {@link Session} to be used * @param location * Challenge location - * @return {@link Challenge} + * @return {@link Challenge} bound to this session and location */ @SuppressWarnings("unchecked") public static T bind(Session session, URI location) throws AcmeException { @@ -78,16 +88,6 @@ public class Challenge extends AcmeResource { } } - /** - * Creates a new generic {@link Challenge} object. - * - * @param session - * {@link Session} to bind to. - */ - public Challenge(Session session) { - super(session); - } - /** * Returns the challenge type by name (e.g. "http-01"). */ @@ -142,7 +142,7 @@ public class Challenge extends AcmeResource { * @return {@code true} if acceptable, {@code false} if not */ protected boolean acceptable(String type) { - return true; + return type != null && !type.trim().isEmpty(); } /** diff --git a/acme4j-client/src/main/java/org/shredzone/acme4j/challenge/Http01Challenge.java b/acme4j-client/src/main/java/org/shredzone/acme4j/challenge/Http01Challenge.java index 47cdd203..deec912d 100644 --- a/acme4j-client/src/main/java/org/shredzone/acme4j/challenge/Http01Challenge.java +++ b/acme4j-client/src/main/java/org/shredzone/acme4j/challenge/Http01Challenge.java @@ -14,7 +14,6 @@ package org.shredzone.acme4j.challenge; import org.shredzone.acme4j.Session; -import org.shredzone.acme4j.util.JSONBuilder; /** * Implements the {@value TYPE} challenge. @@ -57,11 +56,6 @@ public class Http01Challenge extends TokenChallenge { return super.getAuthorization(); } - @Override - protected void respond(JSONBuilder cb) { - super.respond(cb); - } - @Override protected boolean acceptable(String type) { return TYPE.equals(type); diff --git a/acme4j-client/src/main/java/org/shredzone/acme4j/connector/DefaultConnection.java b/acme4j-client/src/main/java/org/shredzone/acme4j/connector/DefaultConnection.java index 4b76dcfa..257d8d20 100644 --- a/acme4j-client/src/main/java/org/shredzone/acme4j/connector/DefaultConnection.java +++ b/acme4j-client/src/main/java/org/shredzone/acme4j/connector/DefaultConnection.java @@ -61,16 +61,32 @@ import org.slf4j.LoggerFactory; public class DefaultConnection implements Connection { private static final Logger LOG = LoggerFactory.getLogger(DefaultConnection.class); - public static final String ACME_ERROR_PREFIX = "urn:ietf:params:acme:error:"; - @Deprecated - public static final String ACME_ERROR_PREFIX_DEPRECATED = "urn:acme:error:"; + public static final String ACME_ERROR_PREFIX = "urn:ietf:params:acme:error:"; + private static final String ACME_ERROR_PREFIX_DEPRECATED = "urn:acme:error:"; + + private static final String ACCEPT_HEADER = "Accept"; + private static final String ACCEPT_CHARSET_HEADER = "Accept-Charset"; + private static final String ACCEPT_LANGUAGE_HEADER = "Accept-Language"; + private static final String CONTENT_TYPE_HEADER = "Content-Type"; + private static final String DATE_HEADER = "Date"; + private static final String LINK_HEADER = "Link"; + private static final String LOCATION_HEADER = "Location"; + private static final String REPLAY_NONCE_HEADER = "Replay-Nonce"; + private static final String RETRY_AFTER_HEADER = "Retry-After"; + private static final String DEFAULT_CHARSET = "utf-8"; private static final Pattern BASE64URL_PATTERN = Pattern.compile("[0-9A-Za-z_-]+"); protected final HttpConnector httpConnector; protected HttpURLConnection conn; + /** + * Creates a new {@link DefaultConnection}. + * + * @param httpConnector + * {@link HttpConnector} to be used for HTTP connections + */ public DefaultConnection(HttpConnector httpConnector) { this.httpConnector = Objects.requireNonNull(httpConnector, "httpConnector"); } @@ -86,8 +102,8 @@ public class DefaultConnection implements Connection { try { conn = httpConnector.openConnection(uri); conn.setRequestMethod("GET"); - conn.setRequestProperty("Accept-Charset", "utf-8"); - conn.setRequestProperty("Accept-Language", session.getLocale().toLanguageTag()); + conn.setRequestProperty(ACCEPT_CHARSET_HEADER, DEFAULT_CHARSET); + conn.setRequestProperty(ACCEPT_LANGUAGE_HEADER, session.getLocale().toLanguageTag()); conn.setDoOutput(false); conn.connect(); @@ -112,7 +128,7 @@ public class DefaultConnection implements Connection { LOG.debug("Getting initial nonce, HEAD {}", uri); conn = httpConnector.openConnection(uri); conn.setRequestMethod("HEAD"); - conn.setRequestProperty("Accept-Language", session.getLocale().toLanguageTag()); + conn.setRequestProperty(ACCEPT_LANGUAGE_HEADER, session.getLocale().toLanguageTag()); conn.connect(); updateSession(session); conn = null; @@ -126,10 +142,10 @@ public class DefaultConnection implements Connection { conn = httpConnector.openConnection(uri); conn.setRequestMethod("POST"); - conn.setRequestProperty("Accept", "application/json"); - conn.setRequestProperty("Accept-Charset", "utf-8"); - conn.setRequestProperty("Accept-Language", session.getLocale().toLanguageTag()); - conn.setRequestProperty("Content-Type", "application/jose+json"); + conn.setRequestProperty(ACCEPT_HEADER, "application/json"); + conn.setRequestProperty(ACCEPT_CHARSET_HEADER, DEFAULT_CHARSET); + conn.setRequestProperty(ACCEPT_LANGUAGE_HEADER, session.getLocale().toLanguageTag()); + conn.setRequestProperty(CONTENT_TYPE_HEADER, "application/jose+json"); conn.setDoOutput(true); final PublicJsonWebKey jwk = PublicJsonWebKey.Factory.newPublicJwk(keypair.getPublic()); @@ -141,7 +157,7 @@ public class DefaultConnection implements Connection { jws.getHeaders().setJwkHeaderValue("jwk", jwk); jws.setAlgorithmHeaderValue(keyAlgorithm(jwk)); jws.setKey(keypair.getPrivate()); - byte[] outputData = jws.getCompactSerialization().getBytes("utf-8"); + byte[] outputData = jws.getCompactSerialization().getBytes(DEFAULT_CHARSET); conn.setFixedLengthStreamingMode(outputData.length); conn.connect(); @@ -172,7 +188,7 @@ public class DefaultConnection implements Connection { } } - if (!"application/problem+json".equals(conn.getHeaderField("Content-Type"))) { + if (!"application/problem+json".equals(conn.getHeaderField(CONTENT_TYPE_HEADER))) { throw new AcmeException("HTTP " + rc + ": " + conn.getResponseMessage()); } @@ -187,7 +203,7 @@ public class DefaultConnection implements Connection { public JSON readJsonResponse() throws AcmeException { assertConnectionIsOpen(); - String contentType = conn.getHeaderField("Content-Type"); + String contentType = conn.getHeaderField(CONTENT_TYPE_HEADER); if (!("application/json".equals(contentType) || "application/problem+json".equals(contentType))) { throw new AcmeProtocolException("Unexpected content type: " + contentType); @@ -214,7 +230,7 @@ public class DefaultConnection implements Connection { public X509Certificate readCertificate() throws AcmeException { assertConnectionIsOpen(); - String contentType = conn.getHeaderField("Content-Type"); + String contentType = conn.getHeaderField(CONTENT_TYPE_HEADER); if (!("application/pkix-cert".equals(contentType))) { throw new AcmeProtocolException("Unexpected content type: " + contentType); } @@ -249,7 +265,7 @@ public class DefaultConnection implements Connection { public void updateSession(Session session) { assertConnectionIsOpen(); - String nonceHeader = conn.getHeaderField("Replay-Nonce"); + String nonceHeader = conn.getHeaderField(REPLAY_NONCE_HEADER); if (nonceHeader == null || nonceHeader.trim().isEmpty()) { return; } @@ -267,7 +283,7 @@ public class DefaultConnection implements Connection { public URI getLocation() { assertConnectionIsOpen(); - String location = conn.getHeaderField("Location"); + String location = conn.getHeaderField(LOCATION_HEADER); if (location == null) { return null; } @@ -296,7 +312,7 @@ public class DefaultConnection implements Connection { List result = new ArrayList<>(); - List links = conn.getHeaderFields().get("Link"); + List links = conn.getHeaderFields().get(LINK_HEADER); if (links != null) { Pattern p = Pattern.compile("<(.*?)>\\s*;\\s*rel=\"?"+ Pattern.quote(relation) + "\"?"); for (String link : links) { @@ -322,7 +338,7 @@ public class DefaultConnection implements Connection { */ private Date getRetryAfterHeader() { // See RFC 2616 section 14.37 - String header = conn.getHeaderField("Retry-After"); + String header = conn.getHeaderField(RETRY_AFTER_HEADER); if (header == null) { return null; } @@ -331,12 +347,12 @@ public class DefaultConnection implements Connection { // delta-seconds if (header.matches("^\\d+$")) { int delta = Integer.parseInt(header); - long date = conn.getHeaderFieldDate("Date", System.currentTimeMillis()); + long date = conn.getHeaderFieldDate(DATE_HEADER, System.currentTimeMillis()); return new Date(date + delta * 1000L); } // HTTP-date - long date = conn.getHeaderFieldDate("Retry-After", 0L); + long date = conn.getHeaderFieldDate(RETRY_AFTER_HEADER, 0L); return date != 0 ? new Date(date) : null; } catch (Exception ex) { throw new AcmeProtocolException("Bad retry-after header value: " + header, ex); 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 ceab5b69..5e543176 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 @@ -51,6 +51,8 @@ public class HttpConnector { /** * Returns the default User-Agent to be used. + * + * @return User-Agent */ public static String defaultUserAgent() { return USER_AGENT; diff --git a/acme4j-client/src/main/java/org/shredzone/acme4j/connector/Resource.java b/acme4j-client/src/main/java/org/shredzone/acme4j/connector/Resource.java index f32d0b69..bf06361f 100644 --- a/acme4j-client/src/main/java/org/shredzone/acme4j/connector/Resource.java +++ b/acme4j-client/src/main/java/org/shredzone/acme4j/connector/Resource.java @@ -24,23 +24,6 @@ public enum Resource { NEW_CERT("new-cert"), REVOKE_CERT("revoke-cert"); - /** - * Parses the string and returns a matching {@link Resource} instance. - * - * @param str - * String to parse - * @return {@link Resource} instance, or {@code null} if the resource is unknown - */ - public static Resource parse(String str) { - for (Resource r : values()) { - if (r.path().equals(str)) { - return r; - } - } - - return null; - } - private final String path; private Resource(String path) { @@ -49,6 +32,8 @@ public enum Resource { /** * Returns the resource path. + * + * @return resource path */ public String path() { return path; 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 fb057423..d83ff9b2 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 @@ -19,6 +19,7 @@ import java.util.ArrayDeque; import java.util.Deque; import java.util.Iterator; import java.util.NoSuchElementException; +import java.util.Objects; import org.shredzone.acme4j.AcmeResource; import org.shredzone.acme4j.Session; @@ -27,8 +28,11 @@ import org.shredzone.acme4j.exception.AcmeProtocolException; import org.shredzone.acme4j.util.JSON; /** - * An {@link Iterator} that fetches a batch of URIs from the ACME server, and - * generates {@link AcmeResource} instances. + * An {@link Iterator} that fetches a batch of URIs from the ACME server, and generates + * {@link AcmeResource} instances. + * + * @param + * {@link AcmeResource} type to iterate over */ public abstract class ResourceIterator implements Iterator { @@ -49,8 +53,8 @@ public abstract class ResourceIterator implements Iterat * URI of the first JSON array, may be {@code null} for an empty iterator */ public ResourceIterator(Session session, String field, URI start) { - this.session = session; - this.field = field; + this.session = Objects.requireNonNull(session, "session"); + this.field = Objects.requireNonNull(field, "field"); this.nextUri = start; } diff --git a/acme4j-client/src/main/java/org/shredzone/acme4j/exception/AcmeConflictException.java b/acme4j-client/src/main/java/org/shredzone/acme4j/exception/AcmeConflictException.java index 9dc60fd8..7926bead 100644 --- a/acme4j-client/src/main/java/org/shredzone/acme4j/exception/AcmeConflictException.java +++ b/acme4j-client/src/main/java/org/shredzone/acme4j/exception/AcmeConflictException.java @@ -14,6 +14,7 @@ package org.shredzone.acme4j.exception; import java.net.URI; +import java.util.Objects; /** * An exception that is thrown when there is a conflict with the request. For example, @@ -24,9 +25,17 @@ public class AcmeConflictException extends AcmeException { private final URI location; + /** + * Creates a new {@link AcmeConflictException}. + * + * @param msg + * Details about the conflicting resource + * @param location + * {@link URI} of the conflicting resource + */ public AcmeConflictException(String msg, URI location) { super(msg); - this.location = location; + this.location = Objects.requireNonNull(location, "location"); } /** diff --git a/acme4j-client/src/main/java/org/shredzone/acme4j/exception/AcmeException.java b/acme4j-client/src/main/java/org/shredzone/acme4j/exception/AcmeException.java index 7fc4902d..2a190429 100644 --- a/acme4j-client/src/main/java/org/shredzone/acme4j/exception/AcmeException.java +++ b/acme4j-client/src/main/java/org/shredzone/acme4j/exception/AcmeException.java @@ -19,14 +19,31 @@ package org.shredzone.acme4j.exception; public class AcmeException extends Exception { private static final long serialVersionUID = -2935088954705632025L; + /** + * Creates a generic {@link AcmeException}. + */ public AcmeException() { super(); } + /** + * Creates a generic {@link AcmeException}. + * + * @param msg + * Description + */ public AcmeException(String msg) { super(msg); } + /** + * Creates a generic {@link AcmeException}. + * + * @param msg + * Description + * @param cause + * {@link Throwable} that caused this exception + */ public AcmeException(String msg, Throwable cause) { super(msg, cause); } diff --git a/acme4j-client/src/main/java/org/shredzone/acme4j/exception/AcmeRetryAfterException.java b/acme4j-client/src/main/java/org/shredzone/acme4j/exception/AcmeRetryAfterException.java index c8d7c904..08c3731b 100644 --- a/acme4j-client/src/main/java/org/shredzone/acme4j/exception/AcmeRetryAfterException.java +++ b/acme4j-client/src/main/java/org/shredzone/acme4j/exception/AcmeRetryAfterException.java @@ -14,6 +14,7 @@ package org.shredzone.acme4j.exception; import java.util.Date; +import java.util.Objects; /** * This exception is thrown when a server side process has not been completed yet, and the @@ -24,16 +25,24 @@ public class AcmeRetryAfterException extends AcmeException { private final Date retryAfter; + /** + * Creates a new {@link AcmeRetryAfterException}. + * + * @param msg + * Error details + * @param retryAfter + * retry-after date returned by the server + */ public AcmeRetryAfterException(String msg, Date retryAfter) { super(msg); - this.retryAfter = retryAfter; + this.retryAfter = Objects.requireNonNull(retryAfter); } /** * Returns the retry-after date returned by the server. */ public Date getRetryAfter() { - return retryAfter != null ? new Date(retryAfter.getTime()) : null; + return new Date(retryAfter.getTime()); } } diff --git a/acme4j-client/src/main/java/org/shredzone/acme4j/exception/AcmeServerException.java b/acme4j-client/src/main/java/org/shredzone/acme4j/exception/AcmeServerException.java index 4e961064..1b2350de 100644 --- a/acme4j-client/src/main/java/org/shredzone/acme4j/exception/AcmeServerException.java +++ b/acme4j-client/src/main/java/org/shredzone/acme4j/exception/AcmeServerException.java @@ -24,6 +24,8 @@ import org.shredzone.acme4j.connector.DefaultConnection; public class AcmeServerException extends AcmeException { private static final long serialVersionUID = 5971622508467042792L; + private static final String ACME_ERROR_PREFIX_DEPRECATED = "urn:acme:error:"; + private final String type; /** @@ -54,12 +56,11 @@ public class AcmeServerException extends AcmeException { * @return ACME error type, or {@code null} if this is not an * {@code "urn:ietf:params:acme:error"} */ - @SuppressWarnings("deprecation") public String getAcmeErrorType() { if (type.startsWith(DefaultConnection.ACME_ERROR_PREFIX)) { return type.substring(DefaultConnection.ACME_ERROR_PREFIX.length()); - } else if (type.startsWith(DefaultConnection.ACME_ERROR_PREFIX_DEPRECATED)) { - return type.substring(DefaultConnection.ACME_ERROR_PREFIX_DEPRECATED.length()); + } else if (type.startsWith(ACME_ERROR_PREFIX_DEPRECATED)) { + return type.substring(ACME_ERROR_PREFIX_DEPRECATED.length()); } else { return null; } diff --git a/acme4j-client/src/main/java/org/shredzone/acme4j/provider/AbstractAcmeProvider.java b/acme4j-client/src/main/java/org/shredzone/acme4j/provider/AbstractAcmeProvider.java index 49e8a464..ddae73ac 100644 --- a/acme4j-client/src/main/java/org/shredzone/acme4j/provider/AbstractAcmeProvider.java +++ b/acme4j-client/src/main/java/org/shredzone/acme4j/provider/AbstractAcmeProvider.java @@ -13,8 +13,13 @@ */ package org.shredzone.acme4j.provider; +import java.lang.reflect.Constructor; +import java.lang.reflect.InvocationTargetException; import java.net.HttpURLConnection; import java.net.URI; +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; import java.util.Objects; import org.shredzone.acme4j.Session; @@ -38,6 +43,8 @@ import org.shredzone.acme4j.util.JSON; */ public abstract class AbstractAcmeProvider implements AcmeProvider { + private static final Map> CHALLENGES = challengeMap(); + @Override public Connection connect() { return new DefaultConnection(createHttpConnector()); @@ -56,22 +63,48 @@ public abstract class AbstractAcmeProvider implements AcmeProvider { } } - @Override @SuppressWarnings("deprecation") // must still provide deprecated challenges + private static Map> challengeMap() { + Map> map = new HashMap<>(); + + try { + map.put(Dns01Challenge.TYPE, + Dns01Challenge.class.getConstructor(Session.class)); + + map.put(org.shredzone.acme4j.challenge.TlsSni01Challenge.TYPE, + org.shredzone.acme4j.challenge.TlsSni01Challenge.class.getConstructor(Session.class)); + + map.put(TlsSni02Challenge.TYPE, + TlsSni02Challenge.class.getConstructor(Session.class)); + + map.put(Http01Challenge.TYPE, + Http01Challenge.class.getConstructor(Session.class)); + + map.put(OutOfBand01Challenge.TYPE, + OutOfBand01Challenge.class.getConstructor(Session.class)); + } catch (NoSuchMethodException ex) { + throw new IllegalStateException("Could not find Challenge constructor", ex); + } + + return Collections.unmodifiableMap(map); + } + + @Override public Challenge createChallenge(Session session, String type) { Objects.requireNonNull(session, "session"); Objects.requireNonNull(type, "type"); - if (type.isEmpty()) { - throw new IllegalArgumentException("no type given"); + + Constructor constructor = CHALLENGES.get(type); + if (constructor == null) { + return null; } - switch (type) { - case Dns01Challenge.TYPE: return new Dns01Challenge(session); - case org.shredzone.acme4j.challenge.TlsSni01Challenge.TYPE: return new org.shredzone.acme4j.challenge.TlsSni01Challenge(session); - case TlsSni02Challenge.TYPE: return new TlsSni02Challenge(session); - case Http01Challenge.TYPE: return new Http01Challenge(session); - case OutOfBand01Challenge.TYPE: return new OutOfBand01Challenge(session); - default: return null; + try { + return constructor.newInstance(session); + } catch (InvocationTargetException | IllegalAccessException + | InstantiationException ex) { + throw new IllegalStateException( + "Could not instantiate a Challenge for type " + type, ex); } } diff --git a/acme4j-client/src/main/java/org/shredzone/acme4j/provider/AcmeProvider.java b/acme4j-client/src/main/java/org/shredzone/acme4j/provider/AcmeProvider.java index 86751ff9..529de286 100644 --- a/acme4j-client/src/main/java/org/shredzone/acme4j/provider/AcmeProvider.java +++ b/acme4j-client/src/main/java/org/shredzone/acme4j/provider/AcmeProvider.java @@ -59,7 +59,7 @@ public interface AcmeProvider { Connection connect(); /** - * Returns the provider's directory. The map must contain resource URIs, and may + * Returns the provider's directory. The structure must contain resource URIs, and may * optionally contain metadata. *

* The default implementation resolves the server URI and fetches the directory via diff --git a/acme4j-client/src/main/java/org/shredzone/acme4j/util/JSON.java b/acme4j-client/src/main/java/org/shredzone/acme4j/util/JSON.java index d3b404aa..84c3a2a6 100644 --- a/acme4j-client/src/main/java/org/shredzone/acme4j/util/JSON.java +++ b/acme4j-client/src/main/java/org/shredzone/acme4j/util/JSON.java @@ -113,13 +113,17 @@ public final class JSON implements Serializable { /** * Returns a {@link JSON} of an empty document. + * + * @return Empty {@link JSON} */ public static JSON empty() { return EMPTY_JSON; } /** - * Returns a {@link Set} of all keys of this object. + * Returns a set of all keys of this object. + * + * @return {@link Set} of keys */ public Set keySet() { return Collections.unmodifiableSet(data.keySet()); @@ -199,6 +203,8 @@ public final class JSON implements Serializable { /** * Returns the array size. + * + * @return Size of the array */ public int size() { return data.size(); @@ -245,8 +251,10 @@ public final class JSON implements Serializable { } /** - * Checks if the value is present. An {@link AcmeProtocolException} is thrown - * if the value is {@code null}. + * Checks if the value is present. An {@link AcmeProtocolException} is thrown if + * the value is {@code null}. + * + * @return itself */ public Value required() { if (val == null) { @@ -256,14 +264,18 @@ public final class JSON implements Serializable { } /** - * Returns the value as {@link String}. May be {@code null}. + * Returns the value as {@link String}. + * + * @return {@link String}, or {@code null} if the value was not set. */ public String asString() { return val != null ? val.toString() : null; } /** - * Returns the value as {@link JSON} object. May be {@code null}. + * Returns the value as JSON object. + * + * @return {@link JSON}, or {@code null} if the value was not set. */ public JSON asObject() { if (val == null) { @@ -278,7 +290,9 @@ public final class JSON implements Serializable { } /** - * Returns the value as JSON {@link Array}. May be {@code null}. + * Returns the value as JSON array. + * + * @return {@link JSON.Array}, or {@code null} if the value was not set. */ public Array asArray() { if (val == null) { @@ -294,6 +308,8 @@ public final class JSON implements Serializable { /** * Returns the value as int. + * + * @return integer value */ public int asInt() { required(); @@ -306,7 +322,9 @@ public final class JSON implements Serializable { } /** - * Returns the value as {@link URI}. May be {@code null}. + * Returns the value as {@link URI}. + * + * @return {@link URI}, or {@code null} if the value was not set. */ public URI asURI() { if (val == null) { @@ -321,7 +339,9 @@ public final class JSON implements Serializable { } /** - * Returns the value as {@link URL}. May be {@code null}. + * Returns the value as {@link URL}. + * + * @return {@link URL}, or {@code null} if the value was not set. */ public URL asURL() { if (val == null) { @@ -336,9 +356,10 @@ public final class JSON implements Serializable { } /** - * Returns the value as {@link Date}. May be {@code null}. The returned - * {@link Date} object is not shared, changes are not reflected in the JSON - * object. + * Returns the value as {@link Date}. + * + * @return {@link Date}, or {@code null} if the value was not set. The returned + * {@link Date} object is not shared and can be modified safely. */ public Date asDate() { if (val == null) { diff --git a/acme4j-client/src/main/java/org/shredzone/acme4j/util/JSONBuilder.java b/acme4j-client/src/main/java/org/shredzone/acme4j/util/JSONBuilder.java index cb07491d..246556f6 100644 --- a/acme4j-client/src/main/java/org/shredzone/acme4j/util/JSONBuilder.java +++ b/acme4j-client/src/main/java/org/shredzone/acme4j/util/JSONBuilder.java @@ -170,6 +170,8 @@ public class JSONBuilder { /** * Returns a {@link Map} representation of the current state. + * + * @return {@link Map} of the current state */ public Map toMap() { return Collections.unmodifiableMap(data); @@ -177,6 +179,8 @@ public class JSONBuilder { /** * Returns a {@link JSON} representation of the current state. + * + * @return {@link JSON} of the current state */ public JSON toJSON() { return JSON.parse(toString()); diff --git a/acme4j-client/src/test/java/org/shredzone/acme4j/SessionTest.java b/acme4j-client/src/test/java/org/shredzone/acme4j/SessionTest.java index adb932c9..73eb9033 100644 --- a/acme4j-client/src/test/java/org/shredzone/acme4j/SessionTest.java +++ b/acme4j-client/src/test/java/org/shredzone/acme4j/SessionTest.java @@ -216,7 +216,7 @@ public class SessionTest { assertThat(meta, not(nullValue())); assertThat(meta.getTermsOfService(), is(nullValue())); assertThat(meta.getWebsite(), is(nullValue())); - assertThat(meta.getCaaIdentities(), is(nullValue())); + assertThat(meta.getCaaIdentities(), is(empty())); } /** diff --git a/acme4j-client/src/test/java/org/shredzone/acme4j/connector/ResourceTest.java b/acme4j-client/src/test/java/org/shredzone/acme4j/connector/ResourceTest.java index df00307f..26074d5d 100644 --- a/acme4j-client/src/test/java/org/shredzone/acme4j/connector/ResourceTest.java +++ b/acme4j-client/src/test/java/org/shredzone/acme4j/connector/ResourceTest.java @@ -13,7 +13,7 @@ */ package org.shredzone.acme4j.connector; -import static org.hamcrest.Matchers.*; +import static org.hamcrest.Matchers.is; import static org.junit.Assert.assertThat; import org.junit.Test; @@ -38,19 +38,4 @@ public class ResourceTest { assertThat(Resource.values().length, is(5)); } - /** - * Test that invoking {@link Resource#parse(String)} with a {@link Resource#path()} - * gives the same {@link Resource}. - */ - @Test - public void testParse() { - for (Resource r : Resource.values()) { - Resource parsed = Resource.parse(r.path()); - assertThat(parsed, is(r)); - } - - // unknown paths return null - assertThat(Resource.parse("foo"), is(nullValue())); - } - } diff --git a/acme4j-client/src/test/java/org/shredzone/acme4j/exception/AcmeRetryAfterExceptionTest.java b/acme4j-client/src/test/java/org/shredzone/acme4j/exception/AcmeRetryAfterExceptionTest.java index 1fbad451..8b047df2 100644 --- a/acme4j-client/src/test/java/org/shredzone/acme4j/exception/AcmeRetryAfterExceptionTest.java +++ b/acme4j-client/src/test/java/org/shredzone/acme4j/exception/AcmeRetryAfterExceptionTest.java @@ -38,6 +38,9 @@ public class AcmeRetryAfterExceptionTest { assertThat(ex.getMessage(), is(detail)); assertThat(ex.getRetryAfter(), is(retryAfter)); + + // make sure we get a copy of the Date object + assertThat(ex.getRetryAfter(), not(sameInstance(retryAfter))); } /** @@ -45,11 +48,24 @@ public class AcmeRetryAfterExceptionTest { */ @Test public void testNullAcmeRetryAfterException() { + Date retryAfter = new Date(System.currentTimeMillis() + 60 * 1000L); + AcmeRetryAfterException ex - = new AcmeRetryAfterException(null, null); + = new AcmeRetryAfterException(null, retryAfter); assertThat(ex.getMessage(), nullValue()); - assertThat(ex.getRetryAfter(), nullValue()); + assertThat(ex.getRetryAfter(), is(retryAfter)); + + // make sure we get a copy of the Date object + assertThat(ex.getRetryAfter(), not(sameInstance(retryAfter))); + } + + /** + * Test that date is required. + */ + @Test(expected = NullPointerException.class) + public void testRequiredAcmeRetryAfterException() { + new AcmeRetryAfterException("null-test", null); } } diff --git a/acme4j-client/src/test/java/org/shredzone/acme4j/provider/AbstractAcmeProviderTest.java b/acme4j-client/src/test/java/org/shredzone/acme4j/provider/AbstractAcmeProviderTest.java index 3f012b92..83be73a3 100644 --- a/acme4j-client/src/test/java/org/shredzone/acme4j/provider/AbstractAcmeProviderTest.java +++ b/acme4j-client/src/test/java/org/shredzone/acme4j/provider/AbstractAcmeProviderTest.java @@ -162,19 +162,15 @@ public class AbstractAcmeProviderTest { assertThat(c7, not(nullValue())); assertThat(c7, instanceOf(OutOfBand01Challenge.class)); + Challenge c8 = provider.createChallenge(session, ""); + assertThat(c8, is(nullValue())); + try { provider.createChallenge(session, (String) null); fail("null was accepted"); } catch (NullPointerException ex) { // expected } - - try { - provider.createChallenge(session, ""); - fail("empty string was accepted"); - } catch (IllegalArgumentException ex) { - // expected - } } } diff --git a/acme4j-utils/src/main/java/org/shredzone/acme4j/util/CSRBuilder.java b/acme4j-utils/src/main/java/org/shredzone/acme4j/util/CSRBuilder.java index 6849681d..1b9f6e3b 100644 --- a/acme4j-utils/src/main/java/org/shredzone/acme4j/util/CSRBuilder.java +++ b/acme4j-utils/src/main/java/org/shredzone/acme4j/util/CSRBuilder.java @@ -59,12 +59,15 @@ public class CSRBuilder { /** * Adds a domain name to the CSR. The first domain name added will also be the - * Common Name. All domain names will be added as Subject - * Alternative Name. + * Common Name. All domain names will be added as Subject Alternative + * Name. *

* IDN domain names are ACE encoded automatically. *

* Note that ACME servers may not accept wildcard domains! + * + * @param domain + * Domain name to add */ public void addDomain(String domain) { String ace = toAce(domain); @@ -78,6 +81,9 @@ public class CSRBuilder { * Adds a {@link Collection} of domains. *

* IDN domain names are ACE encoded automatically. + * + * @param domains + * Collection of domain names to add */ public void addDomains(Collection domains) { for (String domain : domains) { @@ -89,6 +95,9 @@ public class CSRBuilder { * Adds multiple domain names. *

* IDN domain names are ACE encoded automatically. + * + * @param domains + * Domain names to add */ public void addDomains(String... domains) { for (String domain : domains) { @@ -229,7 +238,7 @@ public class CSRBuilder { StringBuilder sb = new StringBuilder(); sb.append(namebuilder.build()); for (String domain : namelist) { - sb.append(",DNS=").append(domain.toString()); + sb.append(",DNS=").append(domain); } return sb.toString(); } diff --git a/acme4j-utils/src/main/java/org/shredzone/acme4j/util/CertificateUtils.java b/acme4j-utils/src/main/java/org/shredzone/acme4j/util/CertificateUtils.java index ddd3a1f2..94ecc550 100644 --- a/acme4j-utils/src/main/java/org/shredzone/acme4j/util/CertificateUtils.java +++ b/acme4j-utils/src/main/java/org/shredzone/acme4j/util/CertificateUtils.java @@ -91,7 +91,7 @@ public final class CertificateUtils { */ public static void writeX509Certificate(X509Certificate cert, Writer w) throws IOException { try (JcaPEMWriter jw = new JcaPEMWriter(w)) { - jw.writeObject(cert); + writeCertIfNotNull(jw, cert); } } @@ -110,19 +110,27 @@ public final class CertificateUtils { public static void writeX509CertificateChain(Writer w, X509Certificate cert, X509Certificate... chain) throws IOException { try (JcaPEMWriter jw = new JcaPEMWriter(w)) { - if (cert != null) { - jw.writeObject(cert); - } - if (chain != null) { - for (X509Certificate c : chain) { - if (c != null) { - jw.writeObject(c); - } - } + writeCertIfNotNull(jw, cert); + for (X509Certificate c : chain) { + writeCertIfNotNull(jw, c); } } } + /** + * Writes an {@link X509Certificate} unless it is {@code null}. + * + * @param jw + * {@link JcaPEMWriter} to write to + * @param cert + * {@link X509Certificate} to write, or {@code null} + */ + private static void writeCertIfNotNull(JcaPEMWriter jw, X509Certificate cert) throws IOException { + if (cert != null) { + jw.writeObject(cert); + } + } + /** * Writes an X.509 certificate chain PEM file. *