Clean up code

pull/30/head
Richard Körber 2016-12-21 23:28:03 +01:00
parent 584452b079
commit 2ce40ec971
27 changed files with 350 additions and 203 deletions

View File

@ -47,23 +47,25 @@ public class Authorization extends AcmeResource {
private List<List<Challenge>> 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<Challenge> fetchChallenges(JSON json) {
JSON.Array jsonChallenges = json.get("challenges").asArray();
List<Challenge> 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<List<Challenge>> fetchCombinations(JSON json, List<Challenge> challenges) {
JSON.Array jsonCombinations = json.get("combinations").asArray();
if (jsonCombinations != null) {
List<List<Challenge>> cmb = new ArrayList<>(jsonCombinations.size());
for (int ix = 0; ix < jsonCombinations.size(); ix++) {
JSON.Array c = jsonCombinations.get(ix).asArray();
List<Challenge> clist = new ArrayList<>(c.size());
for (JSON.Value n : c) {
clist.add(cr.get(n.asInt()));
}
cmb.add(clist);
}
combinations = cmb;
} else {
List<List<Challenge>> cmb = new ArrayList<>(1);
cmb.add(cr);
combinations = cmb;
if (jsonCombinations == null) {
return Arrays.asList(challenges);
}
loaded = true;
List<List<Challenge>> cmb = new ArrayList<>(jsonCombinations.size());
for (JSON.Value v : jsonCombinations) {
JSON.Array c = v.asArray();
List<Challenge> clist = new ArrayList<>(c.size());
for (JSON.Value n : c) {
clist.add(challenges.get(n.asInt()));
}
cmb.add(clist);
}
return cmb;
}
}

View File

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

View File

@ -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<String> getCaaIdentities() {
Array array = meta.get("caa-identities").asArray();
if (array == null) {
return null;
return Collections.emptyList();
}
List<String> result = new ArrayList<>(array.size());

View File

@ -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<URI> 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<Authorization> getAuthorizations() throws AcmeException {
LOG.debug("getAuthorizations");
load();
return new ResourceIterator<Authorization>(getSession(), "authorizations", authorizations) {
return new ResourceIterator<Authorization>(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<Certificate> getCertificates() throws AcmeException {
LOG.debug("getCertificates");
load();
return new ResourceIterator<Certificate>(getSession(), "certificates", certificates) {
return new ResourceIterator<Certificate>(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<URI> 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());

View File

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

View File

@ -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;
}
}

View File

@ -129,6 +129,8 @@ public class Session {
* Returns the {@link AcmeProvider} that is used for this session.
* <p>
* The {@link AcmeProvider} instance is lazily created and cached.
*
* @return {@link AcmeProvider}
*/
public AcmeProvider provider() {
synchronized (this) {

View File

@ -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 extends Challenge> 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();
}
/**

View File

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

View File

@ -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<URI> result = new ArrayList<>();
List<String> links = conn.getHeaderFields().get("Link");
List<String> 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);

View File

@ -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;

View File

@ -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;

View File

@ -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 <T>
* {@link AcmeResource} type to iterate over
*/
public abstract class ResourceIterator<T extends AcmeResource> implements Iterator<T> {
@ -49,8 +53,8 @@ public abstract class ResourceIterator<T extends AcmeResource> 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;
}

View File

@ -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");
}
/**

View File

@ -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);
}

View File

@ -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());
}
}

View File

@ -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;
}

View File

@ -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<String, Constructor<? extends Challenge>> 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<String, Constructor<? extends Challenge>> challengeMap() {
Map<String, Constructor<? extends Challenge>> 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<? extends Challenge> 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);
}
}

View File

@ -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.
* <p>
* The default implementation resolves the server URI and fetches the directory via

View File

@ -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<String> 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) {

View File

@ -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<String, Object> 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());

View File

@ -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()));
}
/**

View File

@ -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()));
}
}

View File

@ -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);
}
}

View File

@ -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
}
}
}

View File

@ -59,12 +59,15 @@ public class CSRBuilder {
/**
* Adds a domain name to the CSR. The first domain name added will also be the
* <em>Common Name</em>. All domain names will be added as <em>Subject
* Alternative Name</em>.
* <em>Common Name</em>. All domain names will be added as <em>Subject Alternative
* Name</em>.
* <p>
* IDN domain names are ACE encoded automatically.
* <p>
* 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.
* <p>
* IDN domain names are ACE encoded automatically.
*
* @param domains
* Collection of domain names to add
*/
public void addDomains(Collection<String> domains) {
for (String domain : domains) {
@ -89,6 +95,9 @@ public class CSRBuilder {
* Adds multiple domain names.
* <p>
* 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();
}

View File

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