Streamline error handling

pull/30/head
Richard Körber 2016-12-21 23:28:44 +01:00
parent 4a2d7c4178
commit 32bfe32077
5 changed files with 72 additions and 47 deletions

View File

@ -50,6 +50,7 @@ import org.shredzone.acme4j.exception.AcmeRateLimitExceededException;
import org.shredzone.acme4j.exception.AcmeRetryAfterException; import org.shredzone.acme4j.exception.AcmeRetryAfterException;
import org.shredzone.acme4j.exception.AcmeServerException; import org.shredzone.acme4j.exception.AcmeServerException;
import org.shredzone.acme4j.exception.AcmeUnauthorizedException; import org.shredzone.acme4j.exception.AcmeUnauthorizedException;
import org.shredzone.acme4j.util.AcmeUtils;
import org.shredzone.acme4j.util.JSON; import org.shredzone.acme4j.util.JSON;
import org.shredzone.acme4j.util.JSONBuilder; import org.shredzone.acme4j.util.JSONBuilder;
import org.slf4j.Logger; import org.slf4j.Logger;
@ -61,10 +62,6 @@ import org.slf4j.LoggerFactory;
public class DefaultConnection implements Connection { public class DefaultConnection implements Connection {
private static final Logger LOG = LoggerFactory.getLogger(DefaultConnection.class); private static final Logger LOG = LoggerFactory.getLogger(DefaultConnection.class);
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_HEADER = "Accept";
private static final String ACCEPT_CHARSET_HEADER = "Accept-Charset"; private static final String ACCEPT_CHARSET_HEADER = "Accept-Charset";
private static final String ACCEPT_LANGUAGE_HEADER = "Accept-Language"; private static final String ACCEPT_LANGUAGE_HEADER = "Accept-Language";
@ -193,7 +190,12 @@ public class DefaultConnection implements Connection {
} }
JSON json = readJsonResponse(); JSON json = readJsonResponse();
throw createAcmeException(rc, json);
if (rc == HttpURLConnection.HTTP_CONFLICT) {
throw new AcmeConflictException(json.get("detail").asString(), getLocation());
}
throw createAcmeException(json);
} catch (IOException ex) { } catch (IOException ex) {
throw new AcmeNetworkException(ex); throw new AcmeNetworkException(ex);
} }
@ -361,45 +363,35 @@ public class DefaultConnection implements Connection {
/** /**
* Handles a problem by throwing an exception. If a JSON problem was returned, an * Handles a problem by throwing an exception. If a JSON problem was returned, an
* {@link AcmeServerException} will be thrown. Otherwise a generic * {@link AcmeServerException} or subtype will be thrown. Otherwise a generic
* {@link AcmeException} is thrown. * {@link AcmeException} is thrown.
*/ */
private AcmeException createAcmeException(int rc, JSON json) { private AcmeException createAcmeException(JSON json) {
String type = json.get("type").asString(); String type = json.get("type").asString();
String detail = json.get("detail").asString(); String detail = json.get("detail").asString();
String error = AcmeUtils.stripErrorPrefix(type);
if (detail == null) {
detail = "general problem";
}
if (rc == HttpURLConnection.HTTP_CONFLICT) {
return new AcmeConflictException(detail, getLocation());
}
if (type == null) { if (type == null) {
return new AcmeException(detail); return new AcmeException(detail);
} }
switch (type) { if ("unauthorized".equals(error)) {
case ACME_ERROR_PREFIX + "unauthorized": return new AcmeUnauthorizedException(type, detail);
case ACME_ERROR_PREFIX_DEPRECATED + "unauthorized":
return new AcmeUnauthorizedException(type, detail);
case ACME_ERROR_PREFIX + "agreementRequired":
case ACME_ERROR_PREFIX_DEPRECATED + "agreementRequired":
String instance = json.get("instance").asString();
return new AcmeAgreementRequiredException(
type, detail, getLink("terms-of-service"),
instance != null ? resolveRelative(instance) : null);
case ACME_ERROR_PREFIX + "rateLimited":
case ACME_ERROR_PREFIX_DEPRECATED + "rateLimited":
return new AcmeRateLimitExceededException(
type, detail, getRetryAfterHeader(), getLinks("rate-limit"));
default:
return new AcmeServerException(type, detail);
} }
if ("agreementRequired".equals(error)) {
URI instance = resolveRelative(json.get("instance").asString());
URI tos = getLink("terms-of-service");
return new AcmeAgreementRequiredException(type, detail, tos, instance);
}
if ("rateLimited".equals(error)) {
Date retryAfter = getRetryAfterHeader();
Collection<URI> rateLimits = getLinks("rate-limit");
return new AcmeRateLimitExceededException(type, detail, retryAfter, rateLimits);
}
return new AcmeServerException(type, detail);
} }
/** /**
@ -439,10 +431,16 @@ public class DefaultConnection implements Connection {
* Resolves a relative link against the connection's last URI. * Resolves a relative link against the connection's last URI.
* *
* @param link * @param link
* Link to resolve. Absolute links are just converted to an URI. * Link to resolve. Absolute links are just converted to an URI. May be
* @return Absolute URI of the given link. * {@code null}.
* @return Absolute URI of the given link, or {@code null} if the link was
* {@code null}.
*/ */
private URI resolveRelative(String link) { private URI resolveRelative(String link) {
if (link == null) {
return null;
}
assertConnectionIsOpen(); assertConnectionIsOpen();
try { try {
return new URL(conn.getURL(), link).toURI(); return new URL(conn.getURL(), link).toURI();

View File

@ -15,7 +15,7 @@ package org.shredzone.acme4j.exception;
import java.util.Objects; import java.util.Objects;
import org.shredzone.acme4j.connector.DefaultConnection; import org.shredzone.acme4j.util.AcmeUtils;
/** /**
* An exception that is thrown when the ACME server returned an error. It contains * An exception that is thrown when the ACME server returned an error. It contains
@ -24,8 +24,6 @@ import org.shredzone.acme4j.connector.DefaultConnection;
public class AcmeServerException extends AcmeException { public class AcmeServerException extends AcmeException {
private static final long serialVersionUID = 5971622508467042792L; private static final long serialVersionUID = 5971622508467042792L;
private static final String ACME_ERROR_PREFIX_DEPRECATED = "urn:acme:error:";
private final String type; private final String type;
/** /**
@ -57,13 +55,7 @@ public class AcmeServerException extends AcmeException {
* {@code "urn:ietf:params:acme:error"} * {@code "urn:ietf:params:acme:error"}
*/ */
public String getAcmeErrorType() { public String getAcmeErrorType() {
if (type.startsWith(DefaultConnection.ACME_ERROR_PREFIX)) { return AcmeUtils.stripErrorPrefix(type);
return type.substring(DefaultConnection.ACME_ERROR_PREFIX.length());
} else if (type.startsWith(ACME_ERROR_PREFIX_DEPRECATED)) {
return type.substring(ACME_ERROR_PREFIX_DEPRECATED.length());
} else {
return null;
}
} }
} }

View File

@ -40,6 +40,8 @@ import org.shredzone.acme4j.exception.AcmeProtocolException;
*/ */
public final class AcmeUtils { public final class AcmeUtils {
private static final char[] HEX = "0123456789abcdef".toCharArray(); private static final char[] HEX = "0123456789abcdef".toCharArray();
private 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 Pattern DATE_PATTERN = Pattern.compile( private static final Pattern DATE_PATTERN = Pattern.compile(
"^(\\d{4})-(\\d{2})-(\\d{2})T" "^(\\d{4})-(\\d{2})-(\\d{2})T"
@ -203,4 +205,26 @@ public final class AcmeUtils {
return cal.getTime(); return cal.getTime();
} }
/**
* Strips the acme error prefix from the error string.
* <p>
* For example, for "urn:ietf:params:acme:error:conflict", "conflict" is returned.
* <p>
* This method also handles the deprecated prefix "urn:acme:error:" that is still in
* use at Let's Encrypt.
*
* @param type
* Error type to strip the prefix from. {@code null} is safe.
* @return Stripped error type, or {@code null} if the prefix was not found.
*/
public static String stripErrorPrefix(String type) {
if (type != null && type.startsWith(ACME_ERROR_PREFIX)) {
return type.substring(ACME_ERROR_PREFIX.length());
} else if (type != null && type.startsWith(ACME_ERROR_PREFIX_DEPRECATED)) {
return type.substring(ACME_ERROR_PREFIX_DEPRECATED.length());
} else {
return null;
}
}
} }

View File

@ -446,7 +446,7 @@ public class DefaultConnectionTest {
} catch (AcmeNetworkException ex) { } catch (AcmeNetworkException ex) {
fail("Did not expect an AcmeNetworkException"); fail("Did not expect an AcmeNetworkException");
} catch (AcmeException ex) { } catch (AcmeException ex) {
assertThat(ex.getMessage(), not(isEmptyOrNullString())); assertThat(ex.getMessage(), isEmptyOrNullString());
} }
verify(mockUrlConnection).getHeaderField("Content-Type"); verify(mockUrlConnection).getHeaderField("Content-Type");

View File

@ -233,6 +233,17 @@ public class AcmeUtilsTest {
} }
} }
/**
* Test that error prefix is correctly removed.
*/
@Test
public void testStripErrorPrefix() {
assertThat(stripErrorPrefix("urn:ietf:params:acme:error:unauthorized"), is("unauthorized"));
assertThat(stripErrorPrefix("urn:acme:error:deprecated"), is("deprecated"));
assertThat(stripErrorPrefix("urn:somethingelse:error:message"), is(nullValue()));
assertThat(stripErrorPrefix(null), is(nullValue()));
}
/** /**
* Matches the given time. * Matches the given time.
*/ */