From 101801260f8ee557897bd6a303bb6037baa7e045 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Richard=20K=C3=B6rber?= Date: Wed, 21 Dec 2016 23:24:49 +0100 Subject: [PATCH] Replace all JSON maps with a JSON type --- .../org/shredzone/acme4j/Authorization.java | 35 +- .../java/org/shredzone/acme4j/Metadata.java | 90 +--- .../org/shredzone/acme4j/Registration.java | 56 +-- .../java/org/shredzone/acme4j/Session.java | 39 +- .../shredzone/acme4j/challenge/Challenge.java | 102 +---- .../challenge/OutOfBand01Challenge.java | 2 +- .../acme4j/challenge/TokenChallenge.java | 6 +- .../acme4j/connector/Connection.java | 6 +- .../acme4j/connector/DefaultConnection.java | 40 +- .../acme4j/connector/ResourceIterator.java | 28 +- .../acme4j/provider/AbstractAcmeProvider.java | 4 +- .../acme4j/provider/AcmeProvider.java | 6 +- .../java/org/shredzone/acme4j/util/JSON.java | 386 ++++++++++++++++++ .../shredzone/acme4j/util/JSONBuilder.java | 7 + .../shredzone/acme4j/AuthorizationTest.java | 22 +- .../shredzone/acme4j/RegistrationTest.java | 50 ++- .../org/shredzone/acme4j/SessionTest.java | 17 +- .../acme4j/challenge/ChallengeTest.java | 88 +--- .../acme4j/challenge/DnsChallengeTest.java | 3 +- .../acme4j/challenge/HttpChallengeTest.java | 5 +- .../challenge/OutOfBandChallengeTest.java | 3 +- .../challenge/TlsSni01ChallengeTest.java | 3 +- .../challenge/TlsSni02ChallengeTest.java | 3 +- .../connector/DefaultConnectionTest.java | 17 +- .../acme4j/connector/DummyConnection.java | 4 +- .../connector/ResourceIteratorTest.java | 22 +- .../acme4j/connector/SessionProviderTest.java | 6 +- .../provider/AbstractAcmeProviderTest.java | 10 +- .../provider/TestableConnectionProvider.java | 8 +- .../acme4j/util/JSONBuilderTest.java | 5 + .../org/shredzone/acme4j/util/JSONTest.java | 285 +++++++++++++ .../org/shredzone/acme4j/util/TestUtils.java | 9 +- .../src/test/resources/json.properties | 10 + 33 files changed, 917 insertions(+), 460 deletions(-) create mode 100644 acme4j-client/src/main/java/org/shredzone/acme4j/util/JSON.java create mode 100644 acme4j-client/src/test/java/org/shredzone/acme4j/util/JSONTest.java 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 5106c7b0..5edbb53d 100644 --- a/acme4j-client/src/main/java/org/shredzone/acme4j/Authorization.java +++ b/acme4j-client/src/main/java/org/shredzone/acme4j/Authorization.java @@ -22,13 +22,13 @@ import java.util.Arrays; import java.util.Collection; import java.util.Date; import java.util.List; -import java.util.Map; import org.shredzone.acme4j.challenge.Challenge; import org.shredzone.acme4j.connector.Connection; import org.shredzone.acme4j.exception.AcmeException; import org.shredzone.acme4j.exception.AcmeProtocolException; import org.shredzone.acme4j.exception.AcmeRetryAfterException; +import org.shredzone.acme4j.util.JSON; import org.shredzone.acme4j.util.JSONBuilder; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -180,7 +180,7 @@ public class Authorization extends AcmeResource { conn.sendRequest(getLocation(), getSession()); int rc = conn.accept(HttpURLConnection.HTTP_OK, HttpURLConnection.HTTP_ACCEPTED); - Map result = conn.readJsonResponse(); + JSON result = conn.readJsonResponse(); unmarshalAuthorization(result); if (rc == HttpURLConnection.HTTP_ACCEPTED) { @@ -231,43 +231,42 @@ public class Authorization extends AcmeResource { * @param json * JSON data */ - @SuppressWarnings("unchecked") - protected void unmarshalAuthorization(Map json) { - this.status = Status.parse((String) json.get("status"), Status.PENDING); + protected void unmarshalAuthorization(JSON json) { + this.status = Status.parse(json.get("status").asString(), Status.PENDING); - String jsonExpires = (String) json.get("expires"); + String jsonExpires = json.get("expires").asString(); if (jsonExpires != null) { expires = parseTimestamp(jsonExpires); } - Map jsonIdentifier = (Map) json.get("identifier"); + JSON jsonIdentifier = json.get("identifier").asObject(); if (jsonIdentifier != null) { - String type = (String) jsonIdentifier.get("type"); + String type = jsonIdentifier.get("type").asString(); if (type != null && !"dns".equals(type)) { throw new AcmeProtocolException("Unknown authorization type: " + type); } - domain = (String) jsonIdentifier.get("value"); + domain = jsonIdentifier.get("value").asString(); } - Collection> jsonChallenges = - (Collection>) json.get("challenges"); + JSON.Array jsonChallenges = json.get("challenges").asArray(); List cr = new ArrayList<>(); - for (Map c : jsonChallenges) { - Challenge ch = getSession().createChallenge(c); + for (JSON.Value c : jsonChallenges) { + Challenge ch = getSession().createChallenge(c.asObject()); if (ch != null) { cr.add(ch); } } challenges = cr; - Collection> jsonCombinations = - (Collection>) json.get("combinations"); + JSON.Array jsonCombinations = json.get("combinations").asArray(); if (jsonCombinations != null) { List> cmb = new ArrayList<>(jsonCombinations.size()); - for (List c : jsonCombinations) { + + for (int ix = 0; ix < jsonCombinations.size(); ix++) { + JSON.Array c = jsonCombinations.get(ix).asArray(); List clist = new ArrayList<>(c.size()); - for (Number n : c) { - clist.add(cr.get(n.intValue())); + for (JSON.Value n : c) { + clist.add(cr.get(n.asInt())); } cmb.add(clist); } 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 7882d255..acb485fc 100644 --- a/acme4j-client/src/main/java/org/shredzone/acme4j/Metadata.java +++ b/acme4j-client/src/main/java/org/shredzone/acme4j/Metadata.java @@ -14,26 +14,20 @@ package org.shredzone.acme4j; import java.net.URI; -import java.net.URISyntaxException; +import java.util.ArrayList; import java.util.Collection; -import java.util.HashMap; -import java.util.Map; +import java.util.List; -import org.shredzone.acme4j.exception.AcmeProtocolException; +import org.shredzone.acme4j.util.JSON; +import org.shredzone.acme4j.util.JSON.Array; +import org.shredzone.acme4j.util.JSON.Value; /** * Contains metadata related to the provider. */ public class Metadata { - private final Map meta; - - /** - * Creates an empty new {@link Metadata} instance. - */ - public Metadata() { - this(new HashMap()); - } + private final JSON meta; /** * Creates a new {@link Metadata} instance. @@ -41,7 +35,7 @@ public class Metadata { * @param meta * JSON map of metadata */ - public Metadata(Map meta) { + public Metadata(JSON meta) { this.meta = meta; } @@ -50,7 +44,7 @@ public class Metadata { * available. */ public URI getTermsOfService() { - return getUri("terms-of-service"); + return meta.get("terms-of-service").asURI(); } /** @@ -58,73 +52,31 @@ public class Metadata { * server. {@code null} if not available. */ public URI getWebsite() { - return getUri("website"); + return meta.get("website").asURI(); } /** - * Returns an array of hostnames, which the ACME server recognises as referring to + * 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. */ - public String[] getCaaIdentities() { - return getStringArray("caa-identities"); - } - - /** - * Gets a custom metadata value, as {@link String}. - * - * @param key - * Key of the meta value - * @return Value as {@link String}, or {@code null} if there is no such key in the - * directory metadata. - */ - public String get(String key) { - Object value = meta.get(key); - return value != null ? value.toString() : null; - } - - /** - * Gets a custom metadata value, as {@link URI}. - * - * @param key - * Key of the meta value - * @return Value as {@link URI}, or {@code null} if there is no such key in the - * directory metadata. - * @throws AcmeProtocolException - * if the value is not an {@link URI} - */ - public URI getUri(String key) { - Object uri = meta.get(key); - try { - return uri != null ? new URI(uri.toString()) : null; - } catch (URISyntaxException ex) { - throw new AcmeProtocolException("Bad URI: " + uri, ex); + public Collection getCaaIdentities() { + Array array = meta.get("caa-identities").asArray(); + if (array == null) { + return null; } - } - /** - * Gets a custom metadata value, as array of {@link String}. - * - * @param key - * Key of the meta value - * @return {@link String} array, or {@code null} if there is no such key in the - * directory metadata. - */ - @SuppressWarnings("unchecked") - public String[] getStringArray(String key) { - Object value = meta.get(key); - if (value != null && value instanceof Collection) { - Collection data = (Collection) value; - return data.toArray(new String[data.size()]); + List result = new ArrayList<>(array.size()); + for (Value v : array) { + result.add(v.asString()); } - return null; + return result; } /** - * Returns the metadata as raw JSON map. - *

- * Do not modify the map or its contents. Changes will have a session-wide effect. + * Returns the JSON representation of the metadata. This is useful for reading + * proprietary metadata properties. */ - public Map getJsonData() { + public JSON getJSON() { return meta; } 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 c09b6e76..20d6b30b 100644 --- a/acme4j-client/src/main/java/org/shredzone/acme4j/Registration.java +++ b/acme4j-client/src/main/java/org/shredzone/acme4j/Registration.java @@ -17,17 +17,14 @@ import static org.shredzone.acme4j.util.AcmeUtils.*; import java.net.HttpURLConnection; import java.net.URI; -import java.net.URISyntaxException; import java.security.KeyPair; import java.security.cert.X509Certificate; import java.util.ArrayList; import java.util.Arrays; -import java.util.Collection; import java.util.Collections; import java.util.Date; import java.util.Iterator; import java.util.List; -import java.util.Map; import java.util.Objects; import org.jose4j.jwk.PublicJsonWebKey; @@ -39,6 +36,7 @@ import org.shredzone.acme4j.connector.ResourceIterator; import org.shredzone.acme4j.exception.AcmeException; import org.shredzone.acme4j.exception.AcmeProtocolException; import org.shredzone.acme4j.exception.AcmeRetryAfterException; +import org.shredzone.acme4j.util.JSON; import org.shredzone.acme4j.util.JSONBuilder; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -162,7 +160,7 @@ public class Registration extends AcmeResource { conn.sendSignedRequest(getLocation(), claims, getSession()); conn.accept(HttpURLConnection.HTTP_CREATED, HttpURLConnection.HTTP_ACCEPTED); - Map json = conn.readJsonResponse(); + JSON json = conn.readJsonResponse(); unmarshal(json, conn); } } @@ -193,7 +191,7 @@ public class Registration extends AcmeResource { conn.sendSignedRequest(getSession().resourceUri(Resource.NEW_AUTHZ), claims, getSession()); conn.accept(HttpURLConnection.HTTP_CREATED); - Map json = conn.readJsonResponse(); + JSON json = conn.readJsonResponse(); Authorization auth = new Authorization(getSession(), conn.getLocation()); auth.unmarshalAuthorization(json); @@ -350,49 +348,23 @@ public class Registration extends AcmeResource { * @param conn * {@link Connection} with headers to be evaluated */ - @SuppressWarnings("unchecked") - private void unmarshal(Map json, Connection conn) { - if (json.containsKey("agreement")) { - try { - this.agreement = new URI((String) json.get("agreement")); - } catch (ClassCastException | URISyntaxException ex) { - throw new AcmeProtocolException("Illegal agreement URI", ex); - } + private void unmarshal(JSON json, Connection conn) { + if (json.contains("agreement")) { + this.agreement = json.get("agreement").asURI(); } - if (json.containsKey("contact")) { + if (json.contains("contact")) { contacts.clear(); - for (Object c : (Collection) json.get("contact")) { - try { - contacts.add(new URI((String) c)); - } catch (ClassCastException | URISyntaxException ex) { - throw new AcmeProtocolException("Illegal contact URI", ex); - } + for (JSON.Value v : json.get("contact").asArray()) { + contacts.add(v.asURI()); } } - if (json.containsKey("authorizations")) { - try { - this.authorizations = new URI((String) json.get("authorizations")); - } catch (ClassCastException | URISyntaxException ex) { - throw new AcmeProtocolException("Illegal authorizations URI", ex); - } - } else { - this.authorizations = null; - } + this.authorizations = json.get("authorizations").asURI(); + this.certificates = json.get("certificates").asURI(); - if (json.containsKey("certificates")) { - try { - this.certificates = new URI((String) json.get("certificates")); - } catch (ClassCastException | URISyntaxException ex) { - throw new AcmeProtocolException("Illegal certificates URI", ex); - } - } else { - this.certificates = null; - } - - if (json.containsKey("status")) { - this.status = Status.parse((String) json.get("status")); + if (json.contains("status")) { + this.status = Status.parse(json.get("status").asString()); } URI location = conn.getLocation(); @@ -490,7 +462,7 @@ public class Registration extends AcmeResource { conn.sendSignedRequest(getLocation(), claims, getSession()); conn.accept(HttpURLConnection.HTTP_ACCEPTED); - Map json = conn.readJsonResponse(); + JSON json = conn.readJsonResponse(); unmarshal(json, conn); } } 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 e9bbcfdb..e524b2e2 100644 --- a/acme4j-client/src/main/java/org/shredzone/acme4j/Session.java +++ b/acme4j-client/src/main/java/org/shredzone/acme4j/Session.java @@ -14,7 +14,6 @@ package org.shredzone.acme4j; import java.net.URI; -import java.net.URISyntaxException; import java.security.KeyPair; import java.util.ArrayList; import java.util.Date; @@ -29,8 +28,8 @@ import org.shredzone.acme4j.challenge.Challenge; import org.shredzone.acme4j.challenge.TokenChallenge; import org.shredzone.acme4j.connector.Resource; import org.shredzone.acme4j.exception.AcmeException; -import org.shredzone.acme4j.exception.AcmeProtocolException; import org.shredzone.acme4j.provider.AcmeProvider; +import org.shredzone.acme4j.util.JSON; /** * A session stores the ACME server URI and the account's key pair. It also tracks @@ -46,7 +45,7 @@ public class Session { private KeyPair keyPair; private AcmeProvider provider; private byte[] nonce; - private Map directoryMap; + private JSON directoryJson; private Metadata metadata; private Locale locale = Locale.getDefault(); protected Date directoryCacheExpiry; @@ -162,17 +161,14 @@ public class Session { * Challenge JSON data * @return {@link Challenge} instance */ - public Challenge createChallenge(Map data) { + public Challenge createChallenge(JSON data) { Objects.requireNonNull(data, "data"); - String type = (String) data.get("type"); - if (type == null || type.isEmpty()) { - throw new IllegalArgumentException("type must not be empty or null"); - } + String type = data.get("type").required().asString(); Challenge challenge = provider().createChallenge(this, type); if (challenge == null) { - if (data.containsKey("token")) { + if (data.contains("token")) { challenge = new TokenChallenge(this); } else { challenge = new Challenge(this); @@ -210,30 +206,25 @@ public class Session { * Reads the provider's directory, then rebuild the resource map. The response is * cached. */ - @SuppressWarnings("unchecked") private void readDirectory() throws AcmeException { synchronized (this) { Date now = new Date(); - if (directoryMap == null || !directoryCacheExpiry.after(now)) { - directoryMap = provider().directory(this, getServerUri()); + if (directoryJson == null || !directoryCacheExpiry.after(now)) { + directoryJson = provider().directory(this, getServerUri()); directoryCacheExpiry = new Date(now.getTime() + 60 * 60 * 1000L); - Object meta = directoryMap.get("meta"); - if (meta != null && meta instanceof Map) { - metadata = new Metadata((Map) meta); + JSON meta = directoryJson.get("meta").asObject(); + if (meta != null) { + metadata = new Metadata(meta); } else { - metadata = new Metadata(); + metadata = new Metadata(JSON.empty()); } resourceMap.clear(); - for (Map.Entry entry : directoryMap.entrySet()) { - Resource res = Resource.parse(entry.getKey()); - if (res != null) { - try { - resourceMap.put(res, new URI(entry.getValue().toString())); - } catch (URISyntaxException ex) { - throw new AcmeProtocolException("Illegal URI for resource " + res, ex); - } + for (Resource res : Resource.values()) { + URI uri = directoryJson.get(res.path()).asURI(); + if (uri != null) { + resourceMap.put(res, uri); } } } 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 c6386445..f912eda3 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 @@ -13,22 +13,11 @@ */ package org.shredzone.acme4j.challenge; -import static org.shredzone.acme4j.util.AcmeUtils.parseTimestamp; - -import java.io.IOException; -import java.io.ObjectInputStream; -import java.io.ObjectOutputStream; import java.net.HttpURLConnection; -import java.net.MalformedURLException; import java.net.URI; -import java.net.URL; import java.util.Date; -import java.util.HashMap; -import java.util.Map; import java.util.Objects; -import org.jose4j.json.JsonUtil; -import org.jose4j.lang.JoseException; import org.shredzone.acme4j.AcmeResource; import org.shredzone.acme4j.Session; import org.shredzone.acme4j.Status; @@ -36,6 +25,7 @@ import org.shredzone.acme4j.connector.Connection; import org.shredzone.acme4j.exception.AcmeException; import org.shredzone.acme4j.exception.AcmeProtocolException; import org.shredzone.acme4j.exception.AcmeRetryAfterException; +import org.shredzone.acme4j.util.JSON; import org.shredzone.acme4j.util.JSONBuilder; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -58,7 +48,7 @@ public class Challenge extends AcmeResource { protected static final String KEY_URI = "uri"; protected static final String KEY_VALIDATED = "validated"; - private transient Map data = new HashMap<>(); + private JSON data = JSON.empty(); /** * Returns a {@link Challenge} object of an existing challenge. @@ -79,8 +69,8 @@ public class Challenge extends AcmeResource { conn.sendRequest(location, session); conn.accept(HttpURLConnection.HTTP_OK, HttpURLConnection.HTTP_ACCEPTED); - Map json = conn.readJsonResponse(); - if (!(json.containsKey("type"))) { + JSON json = conn.readJsonResponse(); + if (!(json.contains("type"))) { throw new IllegalArgumentException("Provided URI is not a challenge URI"); } @@ -102,14 +92,14 @@ public class Challenge extends AcmeResource { * Returns the challenge type by name (e.g. "http-01"). */ public String getType() { - return get(KEY_TYPE); + return data.get(KEY_TYPE).asString(); } /** * Returns the current status of the challenge. */ public Status getStatus() { - return Status.parse((String) get(KEY_STATUS), Status.PENDING); + return Status.parse(data.get(KEY_STATUS).asString(), Status.PENDING); } /** @@ -117,24 +107,21 @@ public class Challenge extends AcmeResource { */ @Override public URI getLocation() { - String uri = get(KEY_URI); - if (uri == null) { - return null; - } - - return URI.create(uri); + return data.get(KEY_URI).asURI(); } /** * Returns the validation date, if returned by the server. */ public Date getValidated() { - String valStr = get(KEY_VALIDATED); - if (valStr != null) { - return parseTimestamp(valStr); - } else { - return null; - } + return data.get(KEY_VALIDATED).asDate(); + } + + /** + * Returns the JSON representation of the challenge data. + */ + protected JSON getJSON() { + return data; } /** @@ -161,11 +148,11 @@ public class Challenge extends AcmeResource { /** * Sets the challenge state to the given JSON map. * - * @param map - * JSON map containing the challenge data + * @param json + * JSON containing the challenge data */ - public void unmarshall(Map map) { - String type = (String) map.get(KEY_TYPE); + public void unmarshall(JSON json) { + String type = json.get(KEY_TYPE).asString(); if (type == null) { throw new IllegalArgumentException("map does not contain a type"); } @@ -173,39 +160,10 @@ public class Challenge extends AcmeResource { throw new AcmeProtocolException("wrong type: " + type); } - data.clear(); - data.putAll(map); + data = json; authorize(); } - /** - * Gets a value from the challenge state. - * - * @param key - * Key - * @return Value, or {@code null} if not set - */ - @SuppressWarnings("unchecked") - protected T get(String key) { - return (T) data.get(key); - } - - /** - * Gets an {@link URL} value from the challenge state. - * - * @param key - * Key - * @return Value, or {@code null} if not set - */ - protected URL getUrl(String key) { - try { - String value = get(key); - return value != null ? new URL(value) : null; - } catch (MalformedURLException ex) { - throw new AcmeProtocolException(key + ": invalid URL", ex); - } - } - /** * Callback that is invoked when the challenge is supposed to compute its * authorization data. @@ -260,24 +218,4 @@ public class Challenge extends AcmeResource { } } - /** - * Serialize the data map in JSON. - */ - private void writeObject(ObjectOutputStream out) throws IOException { - out.writeUTF(JsonUtil.toJson(data)); - out.defaultWriteObject(); - } - - /** - * Deserialize the JSON representation of the data map. - */ - private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException { - try { - data = new HashMap<>(JsonUtil.parseJson(in.readUTF())); - in.defaultReadObject(); - } catch (JoseException ex) { - throw new AcmeProtocolException("Cannot deserialize", ex); - } - } - } diff --git a/acme4j-client/src/main/java/org/shredzone/acme4j/challenge/OutOfBand01Challenge.java b/acme4j-client/src/main/java/org/shredzone/acme4j/challenge/OutOfBand01Challenge.java index be721515..39640517 100644 --- a/acme4j-client/src/main/java/org/shredzone/acme4j/challenge/OutOfBand01Challenge.java +++ b/acme4j-client/src/main/java/org/shredzone/acme4j/challenge/OutOfBand01Challenge.java @@ -43,7 +43,7 @@ public class OutOfBand01Challenge extends Challenge { * challenge. */ public URL getValidationUrl() { - return getUrl("href"); + return getJSON().get("href").asURL(); } } diff --git a/acme4j-client/src/main/java/org/shredzone/acme4j/challenge/TokenChallenge.java b/acme4j-client/src/main/java/org/shredzone/acme4j/challenge/TokenChallenge.java index 64ba9905..2e9d9bed 100644 --- a/acme4j-client/src/main/java/org/shredzone/acme4j/challenge/TokenChallenge.java +++ b/acme4j-client/src/main/java/org/shredzone/acme4j/challenge/TokenChallenge.java @@ -56,11 +56,7 @@ public class TokenChallenge extends Challenge { * Gets the token. */ protected String getToken() { - String token = get(KEY_TOKEN); - if (token == null) { - throw new AcmeProtocolException("Challenge token required, but not set"); - } - return token; + return getJSON().get(KEY_TOKEN).required().asString(); } /** diff --git a/acme4j-client/src/main/java/org/shredzone/acme4j/connector/Connection.java b/acme4j-client/src/main/java/org/shredzone/acme4j/connector/Connection.java index 752d54c7..a83e82b3 100644 --- a/acme4j-client/src/main/java/org/shredzone/acme4j/connector/Connection.java +++ b/acme4j-client/src/main/java/org/shredzone/acme4j/connector/Connection.java @@ -17,11 +17,11 @@ import java.net.URI; import java.security.cert.X509Certificate; import java.util.Collection; import java.util.Date; -import java.util.Map; import org.shredzone.acme4j.Session; import org.shredzone.acme4j.exception.AcmeException; import org.shredzone.acme4j.util.JSONBuilder; +import org.shredzone.acme4j.util.JSON; /** * Connects to the ACME server and offers different methods for invoking the API. @@ -63,9 +63,9 @@ public interface Connection extends AutoCloseable { /** * Reads a server response as JSON data. * - * @return Map containing the parsed JSON data + * @return The JSON response */ - Map readJsonResponse() throws AcmeException; + JSON readJsonResponse() throws AcmeException; /** * Reads a certificate. 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 4f3234de..715cd5b4 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 @@ -15,10 +15,8 @@ package org.shredzone.acme4j.connector; import static org.shredzone.acme4j.util.AcmeUtils.keyAlgorithm; -import java.io.BufferedReader; import java.io.IOException; import java.io.InputStream; -import java.io.InputStreamReader; import java.io.OutputStream; import java.net.HttpURLConnection; import java.net.MalformedURLException; @@ -39,7 +37,6 @@ import java.util.regex.Matcher; import java.util.regex.Pattern; import org.jose4j.base64url.Base64Url; -import org.jose4j.json.JsonUtil; import org.jose4j.jwk.PublicJsonWebKey; import org.jose4j.jws.JsonWebSignature; import org.jose4j.lang.JoseException; @@ -52,6 +49,7 @@ import org.shredzone.acme4j.exception.AcmeProtocolException; import org.shredzone.acme4j.exception.AcmeRateLimitExceededException; import org.shredzone.acme4j.exception.AcmeServerException; import org.shredzone.acme4j.exception.AcmeUnauthorizedException; +import org.shredzone.acme4j.util.JSON; import org.shredzone.acme4j.util.JSONBuilder; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -177,15 +175,15 @@ public class DefaultConnection implements Connection { throw new AcmeException("HTTP " + rc + ": " + conn.getResponseMessage()); } - Map map = readJsonResponse(); - throw createAcmeException(rc, map); + JSON json = readJsonResponse(); + throw createAcmeException(rc, json); } catch (IOException ex) { throw new AcmeNetworkException(ex); } } @Override - public Map readJsonResponse() throws AcmeException { + public JSON readJsonResponse() throws AcmeException { assertConnectionIsOpen(); String contentType = conn.getHeaderField("Content-Type"); @@ -194,41 +192,23 @@ public class DefaultConnection implements Connection { throw new AcmeProtocolException("Unexpected content type: " + contentType); } - Map result = null; + JSON result = null; String response = ""; try { InputStream in = conn.getResponseCode() < 400 ? conn.getInputStream() : conn.getErrorStream(); if (in != null) { - response = readStream(in); - result = JsonUtil.parseJson(response); + result = JSON.parse(in); LOG.debug("Result JSON: {}", response); } } catch (IOException ex) { throw new AcmeNetworkException(ex); - } catch (JoseException ex) { - throw new AcmeProtocolException("Failed to parse response: " + response, ex); } return result; } - private String readStream(InputStream in) throws IOException { - StringBuilder sb = new StringBuilder(); - - try (BufferedReader reader = new BufferedReader(new InputStreamReader(in, "utf-8"))) { - String line = reader.readLine(); - - while (line != null) { - sb.append(line.trim()); - line = reader.readLine(); - } - } - - return sb.toString(); - } - @Override public X509Certificate readCertificate() throws AcmeException { assertConnectionIsOpen(); @@ -351,9 +331,9 @@ public class DefaultConnection implements Connection { * {@link AcmeServerException} will be thrown. Otherwise a generic * {@link AcmeException} is thrown. */ - private AcmeException createAcmeException(int rc, Map map) { - String type = (String) map.get("type"); - String detail = (String) map.get("detail"); + private AcmeException createAcmeException(int rc, JSON json) { + String type = json.get("type").asString(); + String detail = json.get("detail").asString(); if (detail == null) { detail = "general problem"; @@ -374,7 +354,7 @@ public class DefaultConnection implements Connection { case ACME_ERROR_PREFIX + "agreementRequired": case ACME_ERROR_PREFIX_DEPRECATED + "agreementRequired": - String instance = (String) map.get("instance"); + String instance = json.get("instance").asString(); return new AcmeAgreementRequiredException( type, detail, getLink("terms-of-service"), instance != null ? resolveRelative(instance) : null); 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 6123a55b..fb057423 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 @@ -15,18 +15,16 @@ package org.shredzone.acme4j.connector; import java.net.HttpURLConnection; import java.net.URI; -import java.net.URISyntaxException; import java.util.ArrayDeque; -import java.util.Collection; import java.util.Deque; import java.util.Iterator; -import java.util.Map; import java.util.NoSuchElementException; import org.shredzone.acme4j.AcmeResource; import org.shredzone.acme4j.Session; import org.shredzone.acme4j.exception.AcmeException; 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 @@ -147,7 +145,7 @@ public abstract class ResourceIterator implements Iterat conn.sendRequest(nextUri, session); conn.accept(HttpURLConnection.HTTP_OK); - Map json = conn.readJsonResponse(); + JSON json = conn.readJsonResponse(); fillUriList(json); nextUri = conn.getLink("next"); @@ -160,21 +158,13 @@ public abstract class ResourceIterator implements Iterat * @param json * JSON map to read from */ - private void fillUriList(Map json) { - try { - @SuppressWarnings("unchecked") - Collection array = (Collection) json.get(field); - if (array == null) { - return; - } - - for (String uri : array) { - uriList.add(new URI(uri)); - } - } catch (ClassCastException ex) { - throw new AcmeProtocolException("Expected an array", ex); - } catch (URISyntaxException ex) { - throw new AcmeProtocolException("Invalid URI", ex); + private void fillUriList(JSON json) { + JSON.Array array = json.get(field).asArray(); + if (array == null) { + return; + } + for (JSON.Value v : array) { + uriList.add(v.asURI()); } } 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 faa2e11b..49e8a464 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 @@ -15,7 +15,6 @@ package org.shredzone.acme4j.provider; import java.net.HttpURLConnection; import java.net.URI; -import java.util.Map; import java.util.Objects; import org.shredzone.acme4j.Session; @@ -28,6 +27,7 @@ import org.shredzone.acme4j.connector.Connection; import org.shredzone.acme4j.connector.DefaultConnection; import org.shredzone.acme4j.connector.HttpConnector; import org.shredzone.acme4j.exception.AcmeException; +import org.shredzone.acme4j.util.JSON; /** * Abstract implementation of {@link AcmeProvider}. It consists of a challenge @@ -44,7 +44,7 @@ public abstract class AbstractAcmeProvider implements AcmeProvider { } @Override - public Map directory(Session session, URI serverUri) throws AcmeException { + public JSON directory(Session session, URI serverUri) throws AcmeException { try (Connection conn = connect()) { conn.sendRequest(resolve(serverUri), session); conn.accept(HttpURLConnection.HTTP_OK); 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 1c11c252..86751ff9 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 @@ -14,13 +14,13 @@ package org.shredzone.acme4j.provider; import java.net.URI; -import java.util.Map; import java.util.ServiceLoader; import org.shredzone.acme4j.Session; import org.shredzone.acme4j.challenge.Challenge; import org.shredzone.acme4j.connector.Connection; import org.shredzone.acme4j.exception.AcmeException; +import org.shredzone.acme4j.util.JSON; /** * An {@link AcmeProvider} provides methods to be used for communicating with the ACME @@ -69,9 +69,9 @@ public interface AcmeProvider { * {@link Session} to be used * @param serverUri * Server {@link URI} - * @return Map of directory data + * @return Directory data, as JSON object */ - Map directory(Session session, URI serverUri) throws AcmeException; + JSON directory(Session session, URI serverUri) throws AcmeException; /** * Creates a {@link Challenge} instance for the given challenge type. 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 new file mode 100644 index 00000000..d3b404aa --- /dev/null +++ b/acme4j-client/src/main/java/org/shredzone/acme4j/util/JSON.java @@ -0,0 +1,386 @@ +/* + * acme4j - Java ACME client + * + * Copyright (C) 2016 Richard "Shred" Körber + * http://acme4j.shredzone.org + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + */ +package org.shredzone.acme4j.util; + +import static org.shredzone.acme4j.util.AcmeUtils.parseTimestamp; + +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.io.ObjectInputStream; +import java.io.ObjectOutputStream; +import java.io.Serializable; +import java.net.MalformedURLException; +import java.net.URI; +import java.net.URISyntaxException; +import java.net.URL; +import java.util.Collections; +import java.util.Date; +import java.util.HashMap; +import java.util.Iterator; +import java.util.List; +import java.util.Map; +import java.util.NoSuchElementException; +import java.util.Set; + +import org.jose4j.json.JsonUtil; +import org.jose4j.lang.JoseException; +import org.shredzone.acme4j.exception.AcmeProtocolException; + +/** + * A model containing a JSON result. The content is immutable. + */ +@SuppressWarnings("unchecked") +public final class JSON implements Serializable { + private static final long serialVersionUID = 3091273044605709204L; + + private static final JSON EMPTY_JSON = new JSON(new HashMap()); + + private final String path; + private Map data; + + /** + * Creates a new {@link JSON} root object. + * + * @param data + * {@link Map} containing the parsed JSON data + */ + private JSON(Map data) { + this("", data); + } + + /** + * Creates a new {@link JSON} branch object. + * + * @param path + * Path leading to this branch. + * @param data + * {@link Map} containing the parsed JSON data + */ + private JSON(String path, Map data) { + this.path = path; + this.data = data; + } + + /** + * Parses JSON from an {@link InputStream}. + * + * @param in + * {@link InputStream} to read from. Will be closed after use. + * @return {@link JSON} of the read content. + */ + public static JSON parse(InputStream in) throws IOException { + StringBuilder sb = new StringBuilder(); + + try (BufferedReader reader = new BufferedReader(new InputStreamReader(in, "utf-8"))) { + String line = reader.readLine(); + + while (line != null) { + sb.append(line.trim()); + line = reader.readLine(); + } + } + + return parse(sb.toString()); + } + + /** + * Parses JSON from a String. + * + * @param json + * JSON string + * @return {@link JSON} of the read content. + */ + public static JSON parse(String json) { + try { + return new JSON(JsonUtil.parseJson(json)); + } catch (JoseException ex) { + throw new AcmeProtocolException("Bad JSON: " + json, ex); + } + } + + /** + * Returns a {@link JSON} of an empty document. + */ + public static JSON empty() { + return EMPTY_JSON; + } + + /** + * Returns a {@link Set} of all keys of this object. + */ + public Set keySet() { + return Collections.unmodifiableSet(data.keySet()); + } + + /** + * Checks if this object contains the given key. + * + * @param key + * Name of the key to check + * @return {@code true} if the key is present + */ + public boolean contains(String key) { + return data.containsKey(key); + } + + /** + * Returns the {@link Value} of the given key. + * + * @param key + * Key to read + * @return {@link Value} of the key + */ + public Value get(String key) { + return new Value( + path.isEmpty() ? key : path + '.' + key, + data.get(key)); + } + + /** + * Returns the content as JSON string. + */ + @Override + public String toString() { + return JsonUtil.toJson(data); + } + + /** + * Serialize the data map in JSON. + */ + private void writeObject(ObjectOutputStream out) throws IOException { + out.writeUTF(JsonUtil.toJson(data)); + out.defaultWriteObject(); + } + + /** + * Deserialize the JSON representation of the data map. + */ + private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException { + try { + data = new HashMap<>(JsonUtil.parseJson(in.readUTF())); + in.defaultReadObject(); + } catch (JoseException ex) { + throw new AcmeProtocolException("Cannot deserialize", ex); + } + } + + /** + * Represents a JSON array. + */ + public static final class Array implements Iterable { + private final String path; + private final List data; + + /** + * Creates a new {@link Array} object. + * + * @param path + * JSON path to this array. + * @param data + * Array data + */ + private Array(String path, List data) { + this.path = path; + this.data = data; + } + + /** + * Returns the array size. + */ + public int size() { + return data.size(); + } + + /** + * Gets the {@link Value} at the given index. + * + * @param index + * Array index to read from + * @return {@link Value} at this index + */ + public Value get(int index) { + return new Value(path + '[' + index + ']', data.get(index)); + } + + /** + * Creates a new {@link Iterator} that iterates over the array {@link Value}. + */ + @Override + public Iterator iterator() { + return new ValueIterator(this); + } + } + + /** + * A single JSON value. This instance also covers {@code null} values. + */ + public static final class Value { + private final String path; + private final Object val; + + /** + * Creates a new {@link Value}. + * + * @param path + * JSON path to this value + * @param val + * Value, may be {@code null} + */ + private Value(String path, Object val) { + this.path = path; + this.val = val; + } + + /** + * Checks if the value is present. An {@link AcmeProtocolException} is thrown + * if the value is {@code null}. + */ + public Value required() { + if (val == null) { + throw new AcmeProtocolException(path + ": required, but not set"); + } + return this; + } + + /** + * Returns the value as {@link String}. May be {@code null}. + */ + public String asString() { + return val != null ? val.toString() : null; + } + + /** + * Returns the value as {@link JSON} object. May be {@code null}. + */ + public JSON asObject() { + if (val == null) { + return null; + } + + try { + return new JSON(path, (Map) val); + } catch (ClassCastException ex) { + throw new AcmeProtocolException(path + ": expected an object", ex); + } + } + + /** + * Returns the value as JSON {@link Array}. May be {@code null}. + */ + public Array asArray() { + if (val == null) { + return null; + } + + try { + return new Array(path, (List) val); + } catch (ClassCastException ex) { + throw new AcmeProtocolException(path + ": expected an array", ex); + } + } + + /** + * Returns the value as int. + */ + public int asInt() { + required(); + + try { + return ((Number) val).intValue(); + } catch (ClassCastException ex) { + throw new AcmeProtocolException(path + ": bad number " + val, ex); + } + } + + /** + * Returns the value as {@link URI}. May be {@code null}. + */ + public URI asURI() { + if (val == null) { + return null; + } + + try { + return new URI(val.toString()); + } catch (URISyntaxException ex) { + throw new AcmeProtocolException(path + ": bad URI " + val, ex); + } + } + + /** + * Returns the value as {@link URL}. May be {@code null}. + */ + public URL asURL() { + if (val == null) { + return null; + } + + try { + return new URL(val.toString()); + } catch (MalformedURLException ex) { + throw new AcmeProtocolException(path + ": bad URL " + val, ex); + } + } + + /** + * 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. + */ + public Date asDate() { + if (val == null) { + return null; + } + + try { + return parseTimestamp(val.toString()); + } catch (IllegalArgumentException ex) { + throw new AcmeProtocolException(path + ": bad date " + val, ex); + } + } + } + + /** + * An {@link Iterator} over array {@link Value}. + */ + private static class ValueIterator implements Iterator { + private final Array array; + private int index = 0; + + public ValueIterator(Array array) { + this.array = array; + } + + @Override + public boolean hasNext() { + return index < array.size(); + } + + @Override + public Value next() { + if (!hasNext()) { + throw new NoSuchElementException(); + } + return array.get(index++); + } + + @Override + public void remove() { + throw new UnsupportedOperationException(); + } + } + +} 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 0cd062fc..cb07491d 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 @@ -175,6 +175,13 @@ public class JSONBuilder { return Collections.unmodifiableMap(data); } + /** + * Returns a {@link JSON} representation of the current state. + */ + public JSON toJSON() { + return JSON.parse(toString()); + } + /** * Returns a JSON string representation of the current state. */ diff --git a/acme4j-client/src/test/java/org/shredzone/acme4j/AuthorizationTest.java b/acme4j-client/src/test/java/org/shredzone/acme4j/AuthorizationTest.java index 4eac7414..c01b8d7f 100644 --- a/acme4j-client/src/test/java/org/shredzone/acme4j/AuthorizationTest.java +++ b/acme4j-client/src/test/java/org/shredzone/acme4j/AuthorizationTest.java @@ -23,7 +23,6 @@ import java.net.HttpURLConnection; import java.net.URI; import java.util.Collection; import java.util.Date; -import java.util.Map; import java.util.concurrent.atomic.AtomicBoolean; import org.junit.Test; @@ -34,6 +33,7 @@ import org.shredzone.acme4j.challenge.TlsSni02Challenge; import org.shredzone.acme4j.exception.AcmeException; import org.shredzone.acme4j.exception.AcmeRetryAfterException; import org.shredzone.acme4j.provider.TestableConnectionProvider; +import org.shredzone.acme4j.util.JSON; import org.shredzone.acme4j.util.JSONBuilder; /** @@ -132,8 +132,8 @@ public class AuthorizationTest { } @Override - public Map readJsonResponse() { - return getJsonAsMap("updateAuthorizationResponse"); + public JSON readJsonResponse() { + return getJsonAsObject("updateAuthorizationResponse"); } }; @@ -186,8 +186,8 @@ public class AuthorizationTest { } @Override - public Map readJsonResponse() { - return getJsonAsMap("updateAuthorizationResponse"); + public JSON readJsonResponse() { + return getJsonAsObject("updateAuthorizationResponse"); } }; @@ -234,8 +234,8 @@ public class AuthorizationTest { } @Override - public Map readJsonResponse() { - return getJsonAsMap("updateAuthorizationResponse"); + public JSON readJsonResponse() { + return getJsonAsObject("updateAuthorizationResponse"); } @Override @@ -285,9 +285,9 @@ public class AuthorizationTest { TestableConnectionProvider provider = new TestableConnectionProvider() { @Override public void sendSignedRequest(URI uri, JSONBuilder claims, Session session) { - Map claimMap = claims.toMap(); - assertThat(claimMap.get("resource"), is((Object) "authz")); - assertThat(claimMap.get("status"), is((Object) "deactivated")); + JSON json = claims.toJSON(); + assertThat(json.get("resource").asString(), is("authz")); + assertThat(json.get("status").asString(), is("deactivated")); assertThat(uri, is(locationUri)); assertThat(session, is(notNullValue())); } @@ -318,7 +318,7 @@ public class AuthorizationTest { provider.putTestChallenge(TlsSni02Challenge.TYPE, new TlsSni02Challenge(session)); Authorization authorization = new Authorization(session, locationUri); - authorization.unmarshalAuthorization(getJsonAsMap("authorizationChallenges")); + authorization.unmarshalAuthorization(getJsonAsObject("authorizationChallenges")); return authorization; } } diff --git a/acme4j-client/src/test/java/org/shredzone/acme4j/RegistrationTest.java b/acme4j-client/src/test/java/org/shredzone/acme4j/RegistrationTest.java index 39df46b1..9b81c7e0 100644 --- a/acme4j-client/src/test/java/org/shredzone/acme4j/RegistrationTest.java +++ b/acme4j-client/src/test/java/org/shredzone/acme4j/RegistrationTest.java @@ -23,11 +23,8 @@ import java.net.HttpURLConnection; import java.net.URI; import java.security.KeyPair; import java.security.cert.X509Certificate; -import java.util.Arrays; import java.util.Calendar; -import java.util.HashMap; import java.util.Iterator; -import java.util.Map; import java.util.TimeZone; import java.util.concurrent.atomic.AtomicBoolean; @@ -43,6 +40,7 @@ import org.shredzone.acme4j.connector.Resource; import org.shredzone.acme4j.exception.AcmeException; import org.shredzone.acme4j.provider.AcmeProvider; import org.shredzone.acme4j.provider.TestableConnectionProvider; +import org.shredzone.acme4j.util.JSON; import org.shredzone.acme4j.util.JSONBuilder; import org.shredzone.acme4j.util.TestUtils; @@ -62,7 +60,7 @@ public class RegistrationTest { @Test public void testUpdateRegistration() throws AcmeException, IOException { TestableConnectionProvider provider = new TestableConnectionProvider() { - private Map jsonResponse; + private JSON jsonResponse; private Integer response; @Override @@ -70,24 +68,24 @@ public class RegistrationTest { assertThat(uri, is(locationUri)); assertThat(claims.toString(), sameJSONAs(getJson("updateRegistration"))); assertThat(session, is(notNullValue())); - jsonResponse = getJsonAsMap("updateRegistrationResponse"); + jsonResponse = getJsonAsObject("updateRegistrationResponse"); response = HttpURLConnection.HTTP_ACCEPTED; } @Override public void sendRequest(URI uri, Session session) { if (URI.create("https://example.com/acme/reg/1/authz").equals(uri)) { - jsonResponse = new HashMap<>(); - jsonResponse.put("authorizations", - Arrays.asList("https://example.com/acme/auth/1")); + jsonResponse = new JSONBuilder() + .array("authorizations", "https://example.com/acme/auth/1") + .toJSON(); response = HttpURLConnection.HTTP_OK; return; } if (URI.create("https://example.com/acme/reg/1/cert").equals(uri)) { - jsonResponse = new HashMap<>(); - jsonResponse.put("certificates", - Arrays.asList("https://example.com/acme/cert/1")); + jsonResponse = new JSONBuilder() + .array("certificates", "https://example.com/acme/cert/1") + .toJSON(); response = HttpURLConnection.HTTP_OK; return; } @@ -102,7 +100,7 @@ public class RegistrationTest { } @Override - public Map readJsonResponse() { + public JSON readJsonResponse() { return jsonResponse; } @@ -167,8 +165,8 @@ public class RegistrationTest { } @Override - public Map readJsonResponse() { - return getJsonAsMap("updateRegistrationResponse"); + public JSON readJsonResponse() { + return getJsonAsObject("updateRegistrationResponse"); } @Override @@ -221,8 +219,8 @@ public class RegistrationTest { } @Override - public Map readJsonResponse() { - return getJsonAsMap("newAuthorizationResponse"); + public JSON readJsonResponse() { + return getJsonAsObject("newAuthorizationResponse"); } @Override @@ -417,12 +415,12 @@ public class RegistrationTest { assertThat(session, is(notNullValue())); assertThat(session.getKeyPair(), is(sameInstance(oldKeyPair))); - Map json = payload.toMap(); - assertThat((String) json.get("resource"), is("key-change")); // Required by Let's Encrypt + JSON json = payload.toJSON(); + assertThat(json.get("resource").asString(), is("key-change")); // Required by Let's Encrypt - String encodedHeader = (String) json.get("protected"); - String encodedSignature = (String) json.get("signature"); - String encodedPayload = (String) json.get("payload"); + String encodedHeader = json.get("protected").asString(); + String encodedSignature = json.get("signature").asString(); + String encodedPayload = json.get("payload").asString(); String serialized = CompactSerializer.serialize(encodedHeader, encodedPayload, encodedSignature); JsonWebSignature jws = new JsonWebSignature(); @@ -497,9 +495,9 @@ public class RegistrationTest { TestableConnectionProvider provider = new TestableConnectionProvider() { @Override public void sendSignedRequest(URI uri, JSONBuilder claims, Session session) { - Map claimMap = claims.toMap(); - assertThat(claimMap.get("resource"), is((Object) "reg")); - assertThat(claimMap.get("status"), is((Object) "deactivated")); + JSON json = claims.toJSON(); + assertThat(json.get("resource").asString(), is("reg")); + assertThat(json.get("status").asString(), is("deactivated")); assertThat(uri, is(locationUri)); assertThat(session, is(notNullValue())); } @@ -540,8 +538,8 @@ public class RegistrationTest { } @Override - public Map readJsonResponse() { - return getJsonAsMap("modifyRegistrationResponse"); + public JSON readJsonResponse() { + return getJsonAsObject("modifyRegistrationResponse"); } @Override 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 b7567653..adb932c9 100644 --- a/acme4j-client/src/test/java/org/shredzone/acme4j/SessionTest.java +++ b/acme4j-client/src/test/java/org/shredzone/acme4j/SessionTest.java @@ -16,12 +16,12 @@ package org.shredzone.acme4j; import static org.hamcrest.Matchers.*; import static org.junit.Assert.*; import static org.mockito.Mockito.*; +import static org.shredzone.acme4j.util.TestUtils.getJsonAsObject; import java.io.IOException; import java.net.URI; import java.security.KeyPair; import java.util.Date; -import java.util.Map; import org.junit.Test; import org.mockito.ArgumentMatchers; @@ -30,6 +30,7 @@ import org.shredzone.acme4j.challenge.Http01Challenge; import org.shredzone.acme4j.connector.Resource; import org.shredzone.acme4j.exception.AcmeException; import org.shredzone.acme4j.provider.AcmeProvider; +import org.shredzone.acme4j.util.JSON; import org.shredzone.acme4j.util.JSONBuilder; import org.shredzone.acme4j.util.TestUtils; @@ -117,9 +118,9 @@ public class SessionTest { URI serverUri = URI.create(TestUtils.ACME_SERVER_URI); String challengeType = Http01Challenge.TYPE; - Map data = new JSONBuilder() + JSON data = new JSONBuilder() .put("type", challengeType) - .toMap(); + .toJSON(); Http01Challenge mockChallenge = mock(Http01Challenge.class); final AcmeProvider mockProvider = mock(AcmeProvider.class); @@ -155,7 +156,7 @@ public class SessionTest { when(mockProvider.directory( ArgumentMatchers.any(Session.class), ArgumentMatchers.eq(serverUri))) - .thenReturn(TestUtils.getJsonAsMap("directory")); + .thenReturn(getJsonAsObject("directory")); Session session = new Session(serverUri, keyPair) { @Override @@ -193,7 +194,7 @@ public class SessionTest { when(mockProvider.directory( ArgumentMatchers.any(Session.class), ArgumentMatchers.eq(serverUri))) - .thenReturn(TestUtils.getJsonAsMap("directoryNoMeta")); + .thenReturn(getJsonAsObject("directoryNoMeta")); Session session = new Session(serverUri, keyPair) { @Override @@ -239,10 +240,8 @@ public class SessionTest { assertThat(meta, not(nullValue())); assertThat(meta.getTermsOfService(), is(URI.create("https://example.com/acme/terms"))); assertThat(meta.getWebsite(), is(URI.create("https://www.example.com/"))); - assertThat(meta.getCaaIdentities(), is(arrayContaining("example.com"))); - assertThat(meta.get("x-test-string"), is("foobar")); - assertThat(meta.getUri("x-test-uri"), is(URI.create("https://www.example.org"))); - assertThat(meta.getStringArray("x-test-array"), is(arrayContaining("foo", "bar", "barfoo"))); + assertThat(meta.getCaaIdentities(), containsInAnyOrder("example.com")); + assertThat(meta.getJSON(), is(notNullValue())); } } diff --git a/acme4j-client/src/test/java/org/shredzone/acme4j/challenge/ChallengeTest.java b/acme4j-client/src/test/java/org/shredzone/acme4j/challenge/ChallengeTest.java index 8417ace3..f7f526a7 100644 --- a/acme4j-client/src/test/java/org/shredzone/acme4j/challenge/ChallengeTest.java +++ b/acme4j-client/src/test/java/org/shredzone/acme4j/challenge/ChallengeTest.java @@ -19,20 +19,14 @@ import static org.shredzone.acme4j.util.AcmeUtils.parseTimestamp; import static org.shredzone.acme4j.util.TestUtils.*; import static uk.co.datumedge.hamcrest.json.SameJSONAs.sameJSONAs; -import java.io.ByteArrayInputStream; -import java.io.ByteArrayOutputStream; import java.io.IOException; -import java.io.ObjectInputStream; -import java.io.ObjectOutputStream; import java.net.HttpURLConnection; import java.net.MalformedURLException; import java.net.URI; import java.net.URISyntaxException; import java.net.URL; import java.util.Date; -import java.util.Map; -import org.jose4j.json.JsonUtil; import org.jose4j.lang.JoseException; import org.junit.Before; import org.junit.Test; @@ -42,6 +36,7 @@ import org.shredzone.acme4j.exception.AcmeException; import org.shredzone.acme4j.exception.AcmeProtocolException; import org.shredzone.acme4j.exception.AcmeRetryAfterException; import org.shredzone.acme4j.provider.TestableConnectionProvider; +import org.shredzone.acme4j.util.JSON; import org.shredzone.acme4j.util.JSONBuilder; import org.shredzone.acme4j.util.TestUtils; @@ -77,8 +72,8 @@ public class ChallengeTest { } @Override - public Map readJsonResponse() { - return getJsonAsMap("updateHttpChallengeResponse"); + public JSON readJsonResponse() { + return getJsonAsObject("updateHttpChallengeResponse"); } }; @@ -110,24 +105,17 @@ public class ChallengeTest { assertThat(challenge.getValidated(), is(nullValue())); // Unmarshall a challenge JSON - challenge.unmarshall(TestUtils.getJsonAsMap("genericChallenge")); + challenge.unmarshall(getJsonAsObject("genericChallenge")); // Test unmarshalled values assertThat(challenge.getType(), is("generic-01")); assertThat(challenge.getStatus(), is(Status.VALID)); assertThat(challenge.getLocation(), is(new URI("http://example.com/challenge/123"))); assertThat(challenge.getValidated(), is(parseTimestamp("2015-12-12T17:19:36.336785823Z"))); - assertThat((String) challenge.get("type"), is("generic-01")); - assertThat(challenge.getUrl("uri"), is(new URL("http://example.com/challenge/123"))); - assertThat(challenge.get("not-present"), is(nullValue())); - assertThat(challenge.getUrl("not-present-url"), is(nullValue())); - - try { - challenge.getUrl("type"); - fail("bad URL is not detected"); - } catch (AcmeProtocolException ex) { - // expected - } + assertThat(challenge.getJSON().get("type").asString(), is("generic-01")); + assertThat(challenge.getJSON().get("uri").asURL(), is(new URL("http://example.com/challenge/123"))); + assertThat(challenge.getJSON().get("not-present").asString(), is(nullValue())); + assertThat(challenge.getJSON().get("not-present-url").asURL(), is(nullValue())); } /** @@ -138,7 +126,7 @@ public class ChallengeTest { String json = TestUtils.getJson("genericChallenge"); Challenge challenge = new Challenge(session); - challenge.unmarshall(JsonUtil.parseJson(json)); + challenge.unmarshall(JSON.parse(json)); JSONBuilder cb = new JSONBuilder(); challenge.respond(cb); @@ -152,7 +140,7 @@ public class ChallengeTest { @Test(expected = AcmeProtocolException.class) public void testNotAcceptable() throws URISyntaxException { Http01Challenge challenge = new Http01Challenge(session); - challenge.unmarshall(TestUtils.getJsonAsMap("dnsChallenge")); + challenge.unmarshall(getJsonAsObject("dnsChallenge")); } /** @@ -176,15 +164,15 @@ public class ChallengeTest { } @Override - public Map readJsonResponse() { - return getJsonAsMap("triggerHttpChallengeResponse"); + public JSON readJsonResponse() { + return getJsonAsObject("triggerHttpChallengeResponse"); } }; Session session = provider.createSession(); Http01Challenge challenge = new Http01Challenge(session); - challenge.unmarshall(getJsonAsMap("triggerHttpChallenge")); + challenge.unmarshall(getJsonAsObject("triggerHttpChallenge")); challenge.trigger(); @@ -213,15 +201,15 @@ public class ChallengeTest { } @Override - public Map readJsonResponse() { - return getJsonAsMap("updateHttpChallengeResponse"); + public JSON readJsonResponse() { + return getJsonAsObject("updateHttpChallengeResponse"); } }; Session session = provider.createSession(); Challenge challenge = new Http01Challenge(session); - challenge.unmarshall(getJsonAsMap("triggerHttpChallengeResponse")); + challenge.unmarshall(getJsonAsObject("triggerHttpChallengeResponse")); challenge.update(); @@ -252,8 +240,8 @@ public class ChallengeTest { } @Override - public Map readJsonResponse() { - return getJsonAsMap("updateHttpChallengeResponse"); + public JSON readJsonResponse() { + return getJsonAsObject("updateHttpChallengeResponse"); } @Override @@ -265,7 +253,7 @@ public class ChallengeTest { Session session = provider.createSession(); Challenge challenge = new Http01Challenge(session); - challenge.unmarshall(getJsonAsMap("triggerHttpChallengeResponse")); + challenge.unmarshall(getJsonAsObject("triggerHttpChallengeResponse")); try { challenge.update(); @@ -319,8 +307,8 @@ public class ChallengeTest { } @Override - public Map readJsonResponse() { - return getJsonAsMap("updateRegistrationResponse"); + public JSON readJsonResponse() { + return getJsonAsObject("updateRegistrationResponse"); } }; @@ -336,39 +324,7 @@ public class ChallengeTest { @Test(expected = IllegalArgumentException.class) public void testBadUnmarshall() { Challenge challenge = new Challenge(session); - challenge.unmarshall(TestUtils.getJsonAsMap("updateRegistrationResponse")); - } - - /** - * Test that challenge serialization works correctly. - */ - @Test - public void testSerialization() throws IOException, ClassNotFoundException { - Http01Challenge originalChallenge = new Http01Challenge(session); - originalChallenge.unmarshall(TestUtils.getJsonAsMap("httpChallenge")); - - // Serialize - byte[] data; - try (ByteArrayOutputStream out = new ByteArrayOutputStream()) { - try (ObjectOutputStream oos = new ObjectOutputStream(out)) { - oos.writeObject(originalChallenge); - } - data = out.toByteArray(); - } - - // Deserialize - Challenge testChallenge; - try (ByteArrayInputStream in = new ByteArrayInputStream(data)) { - try (ObjectInputStream ois = new ObjectInputStream(in)) { - testChallenge = (Challenge) ois.readObject(); - } - } - - assertThat(testChallenge, not(sameInstance((Challenge) originalChallenge))); - assertThat(testChallenge, is(instanceOf(Http01Challenge.class))); - assertThat(testChallenge.getType(), is(Http01Challenge.TYPE)); - assertThat(testChallenge.getStatus(), is(Status.PENDING)); - assertThat(((Http01Challenge )testChallenge).getToken(), is("rSoI9JpyvFi-ltdnBW0W1DjKstzG7cHixjzcOjwzAEQ")); + challenge.unmarshall(getJsonAsObject("updateRegistrationResponse")); } } diff --git a/acme4j-client/src/test/java/org/shredzone/acme4j/challenge/DnsChallengeTest.java b/acme4j-client/src/test/java/org/shredzone/acme4j/challenge/DnsChallengeTest.java index 63fef8f1..5c36bf9c 100644 --- a/acme4j-client/src/test/java/org/shredzone/acme4j/challenge/DnsChallengeTest.java +++ b/acme4j-client/src/test/java/org/shredzone/acme4j/challenge/DnsChallengeTest.java @@ -15,6 +15,7 @@ package org.shredzone.acme4j.challenge; import static org.hamcrest.Matchers.is; import static org.junit.Assert.assertThat; +import static org.shredzone.acme4j.util.TestUtils.getJsonAsObject; import static uk.co.datumedge.hamcrest.json.SameJSONAs.sameJSONAs; import java.io.IOException; @@ -46,7 +47,7 @@ public class DnsChallengeTest { @Test public void testDnsChallenge() throws IOException { Dns01Challenge challenge = new Dns01Challenge(session); - challenge.unmarshall(TestUtils.getJsonAsMap("dnsChallenge")); + challenge.unmarshall(getJsonAsObject("dnsChallenge")); assertThat(challenge.getType(), is(Dns01Challenge.TYPE)); assertThat(challenge.getStatus(), is(Status.PENDING)); diff --git a/acme4j-client/src/test/java/org/shredzone/acme4j/challenge/HttpChallengeTest.java b/acme4j-client/src/test/java/org/shredzone/acme4j/challenge/HttpChallengeTest.java index 7f54c037..0e048069 100644 --- a/acme4j-client/src/test/java/org/shredzone/acme4j/challenge/HttpChallengeTest.java +++ b/acme4j-client/src/test/java/org/shredzone/acme4j/challenge/HttpChallengeTest.java @@ -15,6 +15,7 @@ package org.shredzone.acme4j.challenge; import static org.hamcrest.Matchers.is; import static org.junit.Assert.assertThat; +import static org.shredzone.acme4j.util.TestUtils.getJsonAsObject; import static uk.co.datumedge.hamcrest.json.SameJSONAs.sameJSONAs; import java.io.IOException; @@ -49,7 +50,7 @@ public class HttpChallengeTest { @Test public void testHttpChallenge() throws IOException { Http01Challenge challenge = new Http01Challenge(session); - challenge.unmarshall(TestUtils.getJsonAsMap("httpChallenge")); + challenge.unmarshall(getJsonAsObject("httpChallenge")); assertThat(challenge.getType(), is(Http01Challenge.TYPE)); assertThat(challenge.getStatus(), is(Status.PENDING)); @@ -69,7 +70,7 @@ public class HttpChallengeTest { @Test(expected = AcmeProtocolException.class) public void testNoTokenSet() { Http01Challenge challenge = new Http01Challenge(session); - challenge.unmarshall(TestUtils.getJsonAsMap("httpNoTokenChallenge")); + challenge.unmarshall(getJsonAsObject("httpNoTokenChallenge")); challenge.getToken(); } diff --git a/acme4j-client/src/test/java/org/shredzone/acme4j/challenge/OutOfBandChallengeTest.java b/acme4j-client/src/test/java/org/shredzone/acme4j/challenge/OutOfBandChallengeTest.java index b239d1a0..dd98d2b2 100644 --- a/acme4j-client/src/test/java/org/shredzone/acme4j/challenge/OutOfBandChallengeTest.java +++ b/acme4j-client/src/test/java/org/shredzone/acme4j/challenge/OutOfBandChallengeTest.java @@ -15,6 +15,7 @@ package org.shredzone.acme4j.challenge; import static org.hamcrest.Matchers.is; import static org.junit.Assert.assertThat; +import static org.shredzone.acme4j.util.TestUtils.getJsonAsObject; import static uk.co.datumedge.hamcrest.json.SameJSONAs.sameJSONAs; import java.io.IOException; @@ -44,7 +45,7 @@ public class OutOfBandChallengeTest { @Test public void testHttpChallenge() throws IOException { OutOfBand01Challenge challenge = new OutOfBand01Challenge(session); - challenge.unmarshall(TestUtils.getJsonAsMap("oobChallenge")); + challenge.unmarshall(getJsonAsObject("oobChallenge")); assertThat(challenge.getType(), is(OutOfBand01Challenge.TYPE)); assertThat(challenge.getStatus(), is(Status.PENDING)); diff --git a/acme4j-client/src/test/java/org/shredzone/acme4j/challenge/TlsSni01ChallengeTest.java b/acme4j-client/src/test/java/org/shredzone/acme4j/challenge/TlsSni01ChallengeTest.java index ae52f795..ace8014d 100644 --- a/acme4j-client/src/test/java/org/shredzone/acme4j/challenge/TlsSni01ChallengeTest.java +++ b/acme4j-client/src/test/java/org/shredzone/acme4j/challenge/TlsSni01ChallengeTest.java @@ -15,6 +15,7 @@ package org.shredzone.acme4j.challenge; import static org.hamcrest.Matchers.is; import static org.junit.Assert.assertThat; +import static org.shredzone.acme4j.util.TestUtils.getJsonAsObject; import static uk.co.datumedge.hamcrest.json.SameJSONAs.sameJSONAs; import java.io.IOException; @@ -47,7 +48,7 @@ public class TlsSni01ChallengeTest { @Test public void testTlsSniChallenge() throws IOException { TlsSni01Challenge challenge = new TlsSni01Challenge(session); - challenge.unmarshall(TestUtils.getJsonAsMap("tlsSniChallenge")); + challenge.unmarshall(getJsonAsObject("tlsSniChallenge")); assertThat(challenge.getType(), is(TlsSni01Challenge.TYPE)); assertThat(challenge.getStatus(), is(Status.PENDING)); diff --git a/acme4j-client/src/test/java/org/shredzone/acme4j/challenge/TlsSni02ChallengeTest.java b/acme4j-client/src/test/java/org/shredzone/acme4j/challenge/TlsSni02ChallengeTest.java index 2afac28d..e04b201a 100644 --- a/acme4j-client/src/test/java/org/shredzone/acme4j/challenge/TlsSni02ChallengeTest.java +++ b/acme4j-client/src/test/java/org/shredzone/acme4j/challenge/TlsSni02ChallengeTest.java @@ -15,6 +15,7 @@ package org.shredzone.acme4j.challenge; import static org.hamcrest.Matchers.is; import static org.junit.Assert.assertThat; +import static org.shredzone.acme4j.util.TestUtils.getJsonAsObject; import static uk.co.datumedge.hamcrest.json.SameJSONAs.sameJSONAs; import java.io.IOException; @@ -46,7 +47,7 @@ public class TlsSni02ChallengeTest { @Test public void testTlsSni02Challenge() throws IOException { TlsSni02Challenge challenge = new TlsSni02Challenge(session); - challenge.unmarshall(TestUtils.getJsonAsMap("tlsSni02Challenge")); + challenge.unmarshall(getJsonAsObject("tlsSni02Challenge")); assertThat(challenge.getType(), is(TlsSni02Challenge.TYPE)); assertThat(challenge.getStatus(), is(Status.PENDING)); diff --git a/acme4j-client/src/test/java/org/shredzone/acme4j/connector/DefaultConnectionTest.java b/acme4j-client/src/test/java/org/shredzone/acme4j/connector/DefaultConnectionTest.java index fd642818..0bc59ca5 100644 --- a/acme4j-client/src/test/java/org/shredzone/acme4j/connector/DefaultConnectionTest.java +++ b/acme4j-client/src/test/java/org/shredzone/acme4j/connector/DefaultConnectionTest.java @@ -44,6 +44,7 @@ import org.shredzone.acme4j.exception.AcmeException; import org.shredzone.acme4j.exception.AcmeNetworkException; import org.shredzone.acme4j.exception.AcmeProtocolException; import org.shredzone.acme4j.exception.AcmeServerException; +import org.shredzone.acme4j.util.JSON; import org.shredzone.acme4j.util.JSONBuilder; import org.shredzone.acme4j.util.TestUtils; @@ -362,11 +363,11 @@ public class DefaultConnectionTest { try (DefaultConnection conn = new DefaultConnection(mockHttpConnection) { @Override - public Map readJsonResponse() { - Map result = new HashMap<>(); + public JSON readJsonResponse() { + JSONBuilder result = new JSONBuilder(); result.put("type", "urn:zombie:error:apocalypse"); result.put("detail", "Zombie apocalypse in progress"); - return result; + return result.toJSON(); }; }) { conn.conn = mockUrlConnection; @@ -397,8 +398,8 @@ public class DefaultConnectionTest { try (DefaultConnection conn = new DefaultConnection(mockHttpConnection) { @Override - public Map readJsonResponse() { - return new HashMap<>(); + public JSON readJsonResponse() { + return JSON.empty(); }; }) { conn.conn = mockUrlConnection; @@ -553,10 +554,10 @@ public class DefaultConnectionTest { try (DefaultConnection conn = new DefaultConnection(mockHttpConnection)) { conn.conn = mockUrlConnection; - Map result = conn.readJsonResponse(); + JSON result = conn.readJsonResponse(); assertThat(result.keySet(), hasSize(2)); - assertThat(result, hasEntry("foo", (Object) 123L)); - assertThat(result, hasEntry("bar", (Object) "a-string")); + assertThat(result.get("foo").asInt(), is(123)); + assertThat(result.get("bar").asString(), is("a-string")); } verify(mockUrlConnection).getHeaderField("Content-Type"); diff --git a/acme4j-client/src/test/java/org/shredzone/acme4j/connector/DummyConnection.java b/acme4j-client/src/test/java/org/shredzone/acme4j/connector/DummyConnection.java index 231fcba6..6172c32f 100644 --- a/acme4j-client/src/test/java/org/shredzone/acme4j/connector/DummyConnection.java +++ b/acme4j-client/src/test/java/org/shredzone/acme4j/connector/DummyConnection.java @@ -17,11 +17,11 @@ import java.net.URI; import java.security.cert.X509Certificate; import java.util.Collection; import java.util.Date; -import java.util.Map; import org.shredzone.acme4j.Session; import org.shredzone.acme4j.exception.AcmeException; import org.shredzone.acme4j.util.JSONBuilder; +import org.shredzone.acme4j.util.JSON; /** * Dummy implementation of {@link Connection} that always fails. Single methods are @@ -45,7 +45,7 @@ public class DummyConnection implements Connection { } @Override - public Map readJsonResponse() { + public JSON readJsonResponse() { throw new UnsupportedOperationException(); } diff --git a/acme4j-client/src/test/java/org/shredzone/acme4j/connector/ResourceIteratorTest.java b/acme4j-client/src/test/java/org/shredzone/acme4j/connector/ResourceIteratorTest.java index 62c31d9c..4f4c5765 100644 --- a/acme4j-client/src/test/java/org/shredzone/acme4j/connector/ResourceIteratorTest.java +++ b/acme4j-client/src/test/java/org/shredzone/acme4j/connector/ResourceIteratorTest.java @@ -23,19 +23,16 @@ import java.net.URI; import java.util.ArrayList; import java.util.Iterator; import java.util.List; -import java.util.Map; import java.util.NoSuchElementException; -import org.jose4j.json.JsonUtil; -import org.jose4j.lang.JoseException; import org.junit.Before; import org.junit.Test; import org.shredzone.acme4j.Authorization; import org.shredzone.acme4j.Session; import org.shredzone.acme4j.exception.AcmeException; -import org.shredzone.acme4j.exception.AcmeProtocolException; import org.shredzone.acme4j.provider.TestableConnectionProvider; import org.shredzone.acme4j.util.JSONBuilder; +import org.shredzone.acme4j.util.JSON; /** * Unit test for {@link ResourceIterator}. @@ -147,19 +144,14 @@ public class ResourceIteratorTest { } @Override - public Map readJsonResponse() { - try { - int start = ix * RESOURCES_PER_PAGE; - int end = (ix + 1) * RESOURCES_PER_PAGE; + public JSON readJsonResponse() { + int start = ix * RESOURCES_PER_PAGE; + int end = (ix + 1) * RESOURCES_PER_PAGE; - JSONBuilder cb = new JSONBuilder(); - cb.array(TYPE, resourceURIs.subList(start, end).toArray()); + JSONBuilder cb = new JSONBuilder(); + cb.array(TYPE, resourceURIs.subList(start, end).toArray()); - // Make sure to use the JSON parser - return JsonUtil.parseJson(cb.toString()); - } catch (JoseException ex) { - throw new AcmeProtocolException("Invalid JSON", ex); - } + return JSON.parse(cb.toString()); } @Override diff --git a/acme4j-client/src/test/java/org/shredzone/acme4j/connector/SessionProviderTest.java b/acme4j-client/src/test/java/org/shredzone/acme4j/connector/SessionProviderTest.java index fe859ff9..73a8e2ec 100644 --- a/acme4j-client/src/test/java/org/shredzone/acme4j/connector/SessionProviderTest.java +++ b/acme4j-client/src/test/java/org/shredzone/acme4j/connector/SessionProviderTest.java @@ -19,7 +19,6 @@ import static org.junit.Assert.assertThat; import java.io.IOException; import java.net.URI; import java.security.KeyPair; -import java.util.Map; import java.util.ServiceLoader; import org.junit.Before; @@ -28,6 +27,7 @@ import org.shredzone.acme4j.Session; import org.shredzone.acme4j.challenge.Challenge; import org.shredzone.acme4j.exception.AcmeException; import org.shredzone.acme4j.provider.AcmeProvider; +import org.shredzone.acme4j.util.JSON; import org.shredzone.acme4j.util.TestUtils; /** @@ -97,7 +97,7 @@ public class SessionProviderTest { } @Override - public Map directory(Session session, URI serverUri) throws AcmeException { + public JSON directory(Session session, URI serverUri) throws AcmeException { throw new UnsupportedOperationException(); } @@ -125,7 +125,7 @@ public class SessionProviderTest { } @Override - public Map directory(Session session, URI serverUri) throws AcmeException { + public JSON directory(Session session, URI serverUri) throws AcmeException { throw new UnsupportedOperationException(); } 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 62026f1c..3f012b92 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 @@ -17,14 +17,13 @@ import static org.hamcrest.Matchers.*; import static org.junit.Assert.*; import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.*; +import static org.shredzone.acme4j.util.TestUtils.getJsonAsObject; import static uk.co.datumedge.hamcrest.json.SameJSONAs.sameJSONAs; import java.net.HttpURLConnection; import java.net.URI; -import java.util.Map; import java.util.concurrent.atomic.AtomicBoolean; -import org.jose4j.json.JsonUtil; import org.junit.Test; import org.shredzone.acme4j.Session; import org.shredzone.acme4j.challenge.Challenge; @@ -35,6 +34,7 @@ import org.shredzone.acme4j.challenge.TlsSni02Challenge; import org.shredzone.acme4j.connector.Connection; import org.shredzone.acme4j.connector.DefaultConnection; import org.shredzone.acme4j.connector.HttpConnector; +import org.shredzone.acme4j.util.JSON; import org.shredzone.acme4j.util.TestUtils; /** @@ -84,7 +84,7 @@ public class AbstractAcmeProviderTest { final Session session = mock(Session.class); when(connection.accept(any(Integer.class))).thenReturn(HttpURLConnection.HTTP_OK); - when(connection.readJsonResponse()).thenReturn(TestUtils.getJsonAsMap("directory")); + when(connection.readJsonResponse()).thenReturn(getJsonAsObject("directory")); AbstractAcmeProvider provider = new AbstractAcmeProvider() { @Override @@ -105,8 +105,8 @@ public class AbstractAcmeProviderTest { } }; - Map map = provider.directory(session, testServerUri); - assertThat(JsonUtil.toJson(map), sameJSONAs(TestUtils.getJson("directory"))); + JSON map = provider.directory(session, testServerUri); + assertThat(map.toString(), sameJSONAs(TestUtils.getJson("directory"))); verify(connection).sendRequest(testResolvedUri, session); verify(connection).accept(any(Integer.class)); diff --git a/acme4j-client/src/test/java/org/shredzone/acme4j/provider/TestableConnectionProvider.java b/acme4j-client/src/test/java/org/shredzone/acme4j/provider/TestableConnectionProvider.java index 935e216d..590d71a6 100644 --- a/acme4j-client/src/test/java/org/shredzone/acme4j/provider/TestableConnectionProvider.java +++ b/acme4j-client/src/test/java/org/shredzone/acme4j/provider/TestableConnectionProvider.java @@ -25,6 +25,7 @@ import org.shredzone.acme4j.connector.DummyConnection; import org.shredzone.acme4j.connector.Resource; import org.shredzone.acme4j.exception.AcmeException; import org.shredzone.acme4j.util.JSONBuilder; +import org.shredzone.acme4j.util.JSON; import org.shredzone.acme4j.util.TestUtils; /** @@ -84,12 +85,11 @@ public class TestableConnectionProvider extends DummyConnection implements AcmeP } @Override - public Map directory(Session session, URI serverUri) throws AcmeException { - Map result = directory.toMap(); - if (result.isEmpty()) { + public JSON directory(Session session, URI serverUri) throws AcmeException { + if (directory.toMap().isEmpty()) { throw new UnsupportedOperationException(); } - return result; + return directory.toJSON(); } @Override diff --git a/acme4j-client/src/test/java/org/shredzone/acme4j/util/JSONBuilderTest.java b/acme4j-client/src/test/java/org/shredzone/acme4j/util/JSONBuilderTest.java index 227606e1..4a4bbca9 100644 --- a/acme4j-client/src/test/java/org/shredzone/acme4j/util/JSONBuilderTest.java +++ b/acme4j-client/src/test/java/org/shredzone/acme4j/util/JSONBuilderTest.java @@ -69,6 +69,11 @@ public class JSONBuilderTest { hasEntry("fooInt", (Object) 456), hasEntry("fooStr", (Object) "String") )); + + JSON json = cb.toJSON(); + assertThat(json.keySet(), hasSize(2)); + assertThat(json.get("fooInt").asInt(), is(456)); + assertThat(json.get("fooStr").asString(), is("String")); } /** diff --git a/acme4j-client/src/test/java/org/shredzone/acme4j/util/JSONTest.java b/acme4j-client/src/test/java/org/shredzone/acme4j/util/JSONTest.java new file mode 100644 index 00000000..518c8a54 --- /dev/null +++ b/acme4j-client/src/test/java/org/shredzone/acme4j/util/JSONTest.java @@ -0,0 +1,285 @@ +/* + * acme4j - Java ACME client + * + * Copyright (C) 2016 Richard "Shred" Körber + * http://acme4j.shredzone.org + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + */ +package org.shredzone.acme4j.util; + +import static org.hamcrest.Matchers.*; +import static org.junit.Assert.*; +import static uk.co.datumedge.hamcrest.json.SameJSONAs.sameJSONAs; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.ObjectInputStream; +import java.io.ObjectOutputStream; +import java.net.MalformedURLException; +import java.net.URI; +import java.net.URL; +import java.util.Calendar; +import java.util.Iterator; +import java.util.NoSuchElementException; +import java.util.TimeZone; + +import org.junit.Test; +import org.shredzone.acme4j.exception.AcmeProtocolException; + +/** + * Unit test for {@link JSON}. + */ +public class JSONTest { + + /** + * Test that an empty {@link JSON} is empty. + */ + @Test + public void testEmpty() { + JSON empty = JSON.empty(); + assertThat(empty.toString(), is("{}")); + } + + /** + * Test parsers. + */ + @Test + public void testParsers() throws IOException { + String json = "{\"foo\":\"a-text\",\n\"bar\":123}"; + + JSON fromString = JSON.parse(json); + assertThat(fromString.toString(), is(sameJSONAs(json))); + + try (InputStream in = new ByteArrayInputStream(json.getBytes("utf-8"))) { + JSON fromStream = JSON.parse(in); + assertThat(fromStream.toString(), is(sameJSONAs(json))); + } + } + + /** + * Test that bad JSON fails. + */ + @Test(expected = AcmeProtocolException.class) + public void testParsersBadJSON() throws IOException { + JSON.parse("This is no JSON."); + } + + /** + * Test all object related methods. + */ + @Test + public void testObject() { + JSON json = TestUtils.getJsonAsObject("json"); + + assertThat(json.keySet(), containsInAnyOrder( + "text", "number", "uri", "url", "date", "array", "collect")); + assertThat(json.contains("text"), is(true)); + assertThat(json.contains("music"), is(false)); + assertThat(json.get("text"), is(notNullValue())); + assertThat(json.get("music"), is(notNullValue())); + } + + /** + * Test all array related methods. + */ + @Test + public void testArray() { + JSON json = TestUtils.getJsonAsObject("json"); + JSON.Array array = json.get("array").asArray(); + + assertThat(array.size(), is(4)); + assertThat(array.get(0), is(notNullValue())); + assertThat(array.get(1), is(notNullValue())); + assertThat(array.get(2), is(notNullValue())); + assertThat(array.get(3), is(notNullValue())); + } + + /** + * Test all array iterator related methods. + */ + @Test + public void testArrayIterator() { + JSON json = TestUtils.getJsonAsObject("json"); + JSON.Array array = json.get("array").asArray(); + + Iterator it = array.iterator(); + assertThat(it, is(notNullValue())); + + assertThat(it.hasNext(), is(true)); + assertThat(it.next().asString(), is("foo")); + + assertThat(it.hasNext(), is(true)); + assertThat(it.next().asInt(), is(987)); + + assertThat(it.hasNext(), is(true)); + assertThat(it.next().asArray().size(), is(3)); + + assertThat(it.hasNext(), is(true)); + try { + it.remove(); + fail("was able to remove from array"); + } catch (UnsupportedOperationException ex) { + // expected + } + assertThat(it.next().asObject(), is(notNullValue())); + + assertThat(it.hasNext(), is(false)); + try { + it.next(); + fail("next past last element"); + } catch (NoSuchElementException ex) { + // expected + } + } + + /** + * Test all getters on existing values. + */ + @Test + public void testGetter() throws MalformedURLException { + Calendar cal = Calendar.getInstance(TimeZone.getTimeZone("UTC")); + cal.clear(); + cal.set(2016, 0, 8); + + JSON json = TestUtils.getJsonAsObject("json"); + + assertThat(json.get("text").asString(), is("lorem ipsum")); + assertThat(json.get("number").asInt(), is(123)); + assertThat(json.get("uri").asURI(), is(URI.create("mailto:foo@example.com"))); + assertThat(json.get("url").asURL(), is(new URL("http://example.com"))); + assertThat(json.get("date").asDate(), is(cal.getTime())); + + JSON.Array array = json.get("array").asArray(); + assertThat(array.get(0).asString(), is("foo")); + assertThat(array.get(1).asInt(), is(987)); + + JSON.Array array2 = array.get(2).asArray(); + assertThat(array2.get(0).asInt(), is(1)); + assertThat(array2.get(1).asInt(), is(2)); + assertThat(array2.get(2).asInt(), is(3)); + + JSON sub = array.get(3).asObject(); + assertThat(sub.get("test").asString(), is("ok")); + } + + /** + * Test that getters are null safe. + */ + @Test + public void testNullGetter() throws MalformedURLException { + JSON json = TestUtils.getJsonAsObject("json"); + + assertThat(json.get("none"), is(notNullValue())); + assertThat(json.get("none").asString(), is(nullValue())); + assertThat(json.get("none").asURI(), is(nullValue())); + assertThat(json.get("none").asURL(), is(nullValue())); + assertThat(json.get("none").asDate(), is(nullValue())); + assertThat(json.get("none").asArray(), is(nullValue())); + assertThat(json.get("none").asObject(), is(nullValue())); + + try { + json.get("none").asInt(); + fail("asInt did not fail"); + } catch (AcmeProtocolException ex) { + // expected + } + + try { + json.get("none").required(); + fail("required did not fail"); + } catch (AcmeProtocolException ex) { + // expected + } + + JSON.Value textv = json.get("text"); + assertThat(textv.required(), is(textv)); + } + + /** + * Test that wrong getters return an exception. + */ + @Test + public void testWrongGetter() throws MalformedURLException { + JSON json = TestUtils.getJsonAsObject("json"); + + try { + json.get("text").asObject(); + fail("no exception was thrown"); + } catch (AcmeProtocolException ex) { + // expected + } + + try { + json.get("text").asArray(); + fail("no exception was thrown"); + } catch (AcmeProtocolException ex) { + // expected + } + + try { + json.get("text").asInt(); + fail("no exception was thrown"); + } catch (AcmeProtocolException ex) { + // expected + } + + try { + json.get("text").asURI(); + fail("no exception was thrown"); + } catch (AcmeProtocolException ex) { + // expected + } + + try { + json.get("text").asURL(); + fail("no exception was thrown"); + } catch (AcmeProtocolException ex) { + // expected + } + + try { + json.get("text").asDate(); + fail("no exception was thrown"); + } catch (AcmeProtocolException ex) { + // expected + } + } + + /** + * Test that serialization works correctly. + */ + @Test + public void testSerialization() throws IOException, ClassNotFoundException { + JSON originalJson = TestUtils.getJsonAsObject("newAuthorizationResponse"); + + // Serialize + byte[] data; + try (ByteArrayOutputStream out = new ByteArrayOutputStream()) { + try (ObjectOutputStream oos = new ObjectOutputStream(out)) { + oos.writeObject(originalJson); + } + data = out.toByteArray(); + } + + // Deserialize + JSON testJson; + try (ByteArrayInputStream in = new ByteArrayInputStream(data)) { + try (ObjectInputStream ois = new ObjectInputStream(in)) { + testJson = (JSON) ois.readObject(); + } + } + + assertThat(testJson, not(sameInstance(originalJson))); + assertThat(testJson.toString(), not(isEmptyOrNullString())); + assertThat(testJson.toString(), is(sameJSONAs(originalJson.toString()))); + } + +} diff --git a/acme4j-client/src/test/java/org/shredzone/acme4j/util/TestUtils.java b/acme4j-client/src/test/java/org/shredzone/acme4j/util/TestUtils.java index 10d9f469..1a36e977 100644 --- a/acme4j-client/src/test/java/org/shredzone/acme4j/util/TestUtils.java +++ b/acme4j-client/src/test/java/org/shredzone/acme4j/util/TestUtils.java @@ -46,7 +46,6 @@ import org.jose4j.base64url.Base64Url; import org.jose4j.json.JsonUtil; import org.jose4j.jwk.JsonWebKey; import org.jose4j.jwk.JsonWebKey.OutputControlLevel; -import org.jose4j.lang.JoseException; import org.shredzone.acme4j.Session; import org.shredzone.acme4j.provider.AcmeProvider; @@ -109,12 +108,8 @@ public final class TestUtils { * JSON resource * @return Parsed JSON resource */ - public static Map getJsonAsMap(String key) { - try { - return JsonUtil.parseJson(getJson(key)); - } catch (JoseException ex) { - throw new RuntimeException("JSON error", ex); - } + public static JSON getJsonAsObject(String key) { + return JSON.parse(getJson(key)); } /** diff --git a/acme4j-client/src/test/resources/json.properties b/acme4j-client/src/test/resources/json.properties index 1eeaff6c..c46e727d 100644 --- a/acme4j-client/src/test/resources/json.properties +++ b/acme4j-client/src/test/resources/json.properties @@ -34,6 +34,16 @@ directoryNoMeta = \ "new-cert": "https://example.com/acme/new-cert"\ } +json = \ + {\ + "text": "lorem ipsum",\ + "number": 123,\ + "uri": "mailto:foo@example.com",\ + "url": "http://example.com",\ + "date": "2016-01-08T00:00:00Z",\ + "array": ["foo", 987, [1, 2, 3], {"test": "ok"}],\ + "collect": ["foo", "bar", "barfoo"]\ + } newRegistration = \ {"resource":"new-reg",\