mirror of https://github.com/shred/acme4j
Split Connection into interface and implementation
parent
a775cf868f
commit
045968a423
|
@ -13,56 +13,20 @@
|
||||||
*/
|
*/
|
||||||
package org.shredzone.acme4j.connector;
|
package org.shredzone.acme4j.connector;
|
||||||
|
|
||||||
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.URI;
|
import java.net.URI;
|
||||||
import java.net.URISyntaxException;
|
|
||||||
import java.security.KeyPair;
|
|
||||||
import java.security.cert.CertificateException;
|
|
||||||
import java.security.cert.CertificateFactory;
|
|
||||||
import java.security.cert.X509Certificate;
|
import java.security.cert.X509Certificate;
|
||||||
import java.util.EnumMap;
|
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import java.util.regex.Pattern;
|
|
||||||
|
|
||||||
import org.jose4j.base64url.Base64Url;
|
|
||||||
import org.jose4j.json.JsonUtil;
|
|
||||||
import org.jose4j.jwk.JsonWebKey;
|
|
||||||
import org.jose4j.jws.AlgorithmIdentifiers;
|
|
||||||
import org.jose4j.jws.JsonWebSignature;
|
|
||||||
import org.jose4j.lang.JoseException;
|
|
||||||
import org.shredzone.acme4j.Account;
|
import org.shredzone.acme4j.Account;
|
||||||
import org.shredzone.acme4j.exception.AcmeException;
|
import org.shredzone.acme4j.exception.AcmeException;
|
||||||
import org.shredzone.acme4j.exception.AcmeServerException;
|
|
||||||
import org.shredzone.acme4j.util.ClaimBuilder;
|
import org.shredzone.acme4j.util.ClaimBuilder;
|
||||||
import org.slf4j.Logger;
|
|
||||||
import org.slf4j.LoggerFactory;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Connects to the ACME server and offers different methods for invoking the API.
|
* Connects to the ACME server and offers different methods for invoking the API.
|
||||||
*
|
*
|
||||||
* @author Richard "Shred" Körber
|
* @author Richard "Shred" Körber
|
||||||
*/
|
*/
|
||||||
public class Connection implements AutoCloseable {
|
public interface Connection extends AutoCloseable {
|
||||||
private static final Logger LOG = LoggerFactory.getLogger(Connection.class);
|
|
||||||
|
|
||||||
private static final Pattern BASE64URL_PATTERN = Pattern.compile("[0-9A-Za-z_-]+");
|
|
||||||
|
|
||||||
protected final HttpConnector httpConnector;
|
|
||||||
protected HttpURLConnection conn;
|
|
||||||
|
|
||||||
public Connection(HttpConnector httpConnector) {
|
|
||||||
this.httpConnector = httpConnector;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void close() {
|
|
||||||
conn = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Forcedly starts a new {@link Session}. Usually this method is not required, as a
|
* Forcedly starts a new {@link Session}. Usually this method is not required, as a
|
||||||
|
@ -73,18 +37,7 @@ public class Connection implements AutoCloseable {
|
||||||
* @param session
|
* @param session
|
||||||
* {@link Session} instance to be used for tracking
|
* {@link Session} instance to be used for tracking
|
||||||
*/
|
*/
|
||||||
public void startSession(URI uri, Session session) throws AcmeException {
|
void startSession(URI uri, Session session) throws AcmeException;
|
||||||
try {
|
|
||||||
LOG.debug("Initial replay nonce from {}", uri);
|
|
||||||
HttpURLConnection localConn = httpConnector.openConnection(uri);
|
|
||||||
localConn.setRequestMethod("HEAD");
|
|
||||||
localConn.connect();
|
|
||||||
|
|
||||||
session.setNonce(getNonceFromHeader(localConn));
|
|
||||||
} catch (IOException ex) {
|
|
||||||
throw new AcmeException("Failed to request a nonce", ex);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Sends a simple GET request.
|
* Sends a simple GET request.
|
||||||
|
@ -93,24 +46,7 @@ public class Connection implements AutoCloseable {
|
||||||
* {@link URI} to send the request to.
|
* {@link URI} to send the request to.
|
||||||
* @return HTTP response code
|
* @return HTTP response code
|
||||||
*/
|
*/
|
||||||
public int sendRequest(URI uri) throws AcmeException {
|
int sendRequest(URI uri) throws AcmeException;
|
||||||
try {
|
|
||||||
LOG.debug("GET {}", uri);
|
|
||||||
|
|
||||||
conn = httpConnector.openConnection(uri);
|
|
||||||
conn.setRequestMethod("GET");
|
|
||||||
conn.setRequestProperty("Accept-Charset", "utf-8");
|
|
||||||
conn.setDoOutput(false);
|
|
||||||
|
|
||||||
conn.connect();
|
|
||||||
|
|
||||||
throwException();
|
|
||||||
|
|
||||||
return conn.getResponseCode();
|
|
||||||
} catch (IOException ex) {
|
|
||||||
throw new AcmeException("API access failed", ex);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Sends a signed POST request.
|
* Sends a signed POST request.
|
||||||
|
@ -125,209 +61,40 @@ public class Connection implements AutoCloseable {
|
||||||
* {@link Account} to be used for signing the request
|
* {@link Account} to be used for signing the request
|
||||||
* @return HTTP response code
|
* @return HTTP response code
|
||||||
*/
|
*/
|
||||||
public int sendSignedRequest(URI uri, ClaimBuilder claims, Session session, Account account) throws AcmeException {
|
int sendSignedRequest(URI uri, ClaimBuilder claims, Session session, Account account) throws AcmeException;
|
||||||
try {
|
|
||||||
KeyPair keypair = account.getKeyPair();
|
|
||||||
|
|
||||||
if (session.getNonce() == null) {
|
|
||||||
startSession(uri, session);
|
|
||||||
}
|
|
||||||
if (session.getNonce() == null) {
|
|
||||||
throw new AcmeException("No nonce available");
|
|
||||||
}
|
|
||||||
|
|
||||||
LOG.debug("POST {} with claims: {}", uri, claims);
|
|
||||||
|
|
||||||
conn = httpConnector.openConnection(uri);
|
|
||||||
conn.setRequestMethod("POST");
|
|
||||||
conn.setRequestProperty("Accept", "application/json");
|
|
||||||
conn.setRequestProperty("Accept-Charset", "utf-8");
|
|
||||||
conn.setRequestProperty("Content-Type", "application/json");
|
|
||||||
conn.setDoOutput(true);
|
|
||||||
|
|
||||||
final JsonWebKey jwk = JsonWebKey.Factory.newJwk(keypair.getPublic());
|
|
||||||
|
|
||||||
JsonWebSignature jws = new JsonWebSignature();
|
|
||||||
jws.setPayload(claims.toString());
|
|
||||||
jws.getHeaders().setObjectHeaderValue("nonce", Base64Url.encode(session.getNonce()));
|
|
||||||
jws.getHeaders().setJwkHeaderValue("jwk", jwk);
|
|
||||||
jws.setAlgorithmHeaderValue(AlgorithmIdentifiers.RSA_USING_SHA256);
|
|
||||||
jws.setKey(keypair.getPrivate());
|
|
||||||
byte[] outputData = jws.getCompactSerialization().getBytes("utf-8");
|
|
||||||
|
|
||||||
conn.setFixedLengthStreamingMode(outputData.length);
|
|
||||||
conn.connect();
|
|
||||||
|
|
||||||
try (OutputStream out = conn.getOutputStream()) {
|
|
||||||
out.write(outputData);
|
|
||||||
}
|
|
||||||
|
|
||||||
session.setNonce(getNonceFromHeader(conn));
|
|
||||||
|
|
||||||
throwException();
|
|
||||||
|
|
||||||
return conn.getResponseCode();
|
|
||||||
} catch (JoseException | IOException ex) {
|
|
||||||
throw new AcmeException("Failed to send request to " + uri, ex);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Reads a server response as JSON data.
|
* Reads a server response as JSON data.
|
||||||
*
|
*
|
||||||
* @return Map containing the parsed JSON data
|
* @return Map containing the parsed JSON data
|
||||||
*/
|
*/
|
||||||
public Map<String, Object> readJsonResponse() throws AcmeException {
|
Map<String, Object> readJsonResponse() throws AcmeException;
|
||||||
if (conn == null) {
|
|
||||||
throw new IllegalStateException("Not connected");
|
|
||||||
}
|
|
||||||
|
|
||||||
String contentType = conn.getHeaderField("Content-Type");
|
|
||||||
if (!("application/json".equals(contentType)
|
|
||||||
|| "application/problem+json".equals(contentType))) {
|
|
||||||
throw new AcmeException("Unexpected content type: " + contentType);
|
|
||||||
}
|
|
||||||
|
|
||||||
StringBuilder sb = new StringBuilder();
|
|
||||||
Map<String, Object> result = null;
|
|
||||||
|
|
||||||
try {
|
|
||||||
InputStream in = (conn.getResponseCode() < 400 ? conn.getInputStream() : conn.getErrorStream());
|
|
||||||
if (in != null) {
|
|
||||||
try (BufferedReader r = new BufferedReader(new InputStreamReader(in, "utf-8"))) {
|
|
||||||
sb.append(r.readLine());
|
|
||||||
}
|
|
||||||
|
|
||||||
result = JsonUtil.parseJson(sb.toString());
|
|
||||||
LOG.debug("Result JSON: {}", sb);
|
|
||||||
}
|
|
||||||
|
|
||||||
} catch (JoseException | IOException ex) {
|
|
||||||
throw new AcmeException("Failed to parse response: " + sb, ex);
|
|
||||||
}
|
|
||||||
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Reads a certificate.
|
* Reads a certificate.
|
||||||
*
|
*
|
||||||
* @return {@link X509Certificate} that was read.
|
* @return {@link X509Certificate} that was read.
|
||||||
*/
|
*/
|
||||||
public X509Certificate readCertificate() throws AcmeException {
|
X509Certificate readCertificate() throws AcmeException;
|
||||||
if (conn == null) {
|
|
||||||
throw new IllegalStateException("Not connected");
|
|
||||||
}
|
|
||||||
|
|
||||||
String contentType = conn.getHeaderField("Content-Type");
|
|
||||||
if (!("application/pkix-cert".equals(contentType))) {
|
|
||||||
throw new AcmeException("Unexpected content type: " + contentType);
|
|
||||||
}
|
|
||||||
|
|
||||||
try (InputStream in = conn.getInputStream()) {
|
|
||||||
CertificateFactory cf = CertificateFactory.getInstance("X.509");
|
|
||||||
return (X509Certificate) cf.generateCertificate(in);
|
|
||||||
} catch (IOException ex) {
|
|
||||||
throw new AcmeException("Failed to read certificate", ex);
|
|
||||||
} catch (CertificateException ex) {
|
|
||||||
throw new AcmeException("Error while generating the X.509 certificate", ex);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Reads a resource directory.
|
* Reads a resource directory.
|
||||||
*
|
*
|
||||||
* @return Map of {@link Resource} and the respective {@link URI} to invoke
|
* @return Map of {@link Resource} and the respective {@link URI} to invoke
|
||||||
*/
|
*/
|
||||||
public Map<Resource, URI> readDirectory() throws AcmeException {
|
Map<Resource, URI> readDirectory() throws AcmeException;
|
||||||
String contentType = conn.getHeaderField("Content-Type");
|
|
||||||
if (!("application/json".equals(contentType))) {
|
|
||||||
throw new AcmeException("Unexpected content type: " + contentType);
|
|
||||||
}
|
|
||||||
|
|
||||||
EnumMap<Resource, URI> resourceMap = new EnumMap<>(Resource.class);
|
|
||||||
|
|
||||||
StringBuilder sb = new StringBuilder();
|
|
||||||
try (BufferedReader reader = new BufferedReader(new InputStreamReader(conn.getInputStream(), "utf-8"))) {
|
|
||||||
sb.append(reader.readLine());
|
|
||||||
} catch (IOException ex) {
|
|
||||||
throw new AcmeException("Could not read resource map", ex);
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
Map<String, Object> result = JsonUtil.parseJson(sb.toString());
|
|
||||||
for (Map.Entry<String, Object> entry : result.entrySet()) {
|
|
||||||
Resource res = Resource.parse(entry.getKey());
|
|
||||||
if (res != null) {
|
|
||||||
URI uri = new URI(entry.getValue().toString());
|
|
||||||
resourceMap.put(res, uri);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
LOG.debug("Resource directory: {}", resourceMap);
|
|
||||||
} catch (JoseException | URISyntaxException ex) {
|
|
||||||
throw new AcmeException("Could not parse resource map: " + sb, ex);
|
|
||||||
}
|
|
||||||
|
|
||||||
return resourceMap;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Gets a location from the {@code Location} header.
|
* Gets a location from the {@code Location} header.
|
||||||
*
|
*
|
||||||
* @return Location {@link URI}, or {@code null} if no Location header was set
|
* @return Location {@link URI}, or {@code null} if no Location header was set
|
||||||
*/
|
*/
|
||||||
public URI getLocation() throws AcmeException {
|
URI getLocation() throws AcmeException;
|
||||||
String location = conn.getHeaderField("Location");
|
|
||||||
if (location == null) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
LOG.debug("Location: {}", location);
|
|
||||||
return new URI(location);
|
|
||||||
} catch (URISyntaxException ex) {
|
|
||||||
throw new AcmeException("Bad Location header: " + location);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Checks if the server returned an error, and if so, throws a {@link AcmeException}.
|
* Closes the {@link Connection}, releasing all resources.
|
||||||
*
|
|
||||||
* @throws AcmeException
|
|
||||||
* if the server returned a JSON problem
|
|
||||||
*/
|
*/
|
||||||
protected void throwException() throws AcmeException {
|
@Override
|
||||||
if ("application/problem+json".equals(conn.getHeaderField("Content-Type"))) {
|
void close();
|
||||||
Map<String, Object> map = readJsonResponse();
|
|
||||||
String type = (String) map.get("type");
|
|
||||||
String detail = (String) map.get("detail");
|
|
||||||
throw new AcmeServerException(type, detail);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Extracts a nonce from the header.
|
|
||||||
*
|
|
||||||
* @param localConn
|
|
||||||
* {@link HttpURLConnection} to get the nonce from
|
|
||||||
* @return Nonce
|
|
||||||
* @throws AcmeException
|
|
||||||
* if there was no {@code Replay-Nonce} header, or the nonce was invalid
|
|
||||||
*/
|
|
||||||
protected byte[] getNonceFromHeader(HttpURLConnection localConn) throws AcmeException {
|
|
||||||
String nonceHeader = localConn.getHeaderField("Replay-Nonce");
|
|
||||||
if (nonceHeader == null || nonceHeader.trim().isEmpty()) {
|
|
||||||
throw new AcmeException("No replay nonce");
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!BASE64URL_PATTERN.matcher(nonceHeader).matches()) {
|
|
||||||
throw new AcmeException("Invalid replay nonce: " + nonceHeader);
|
|
||||||
}
|
|
||||||
|
|
||||||
LOG.debug("Replay Nonce: {}", nonceHeader);
|
|
||||||
|
|
||||||
return Base64Url.decode(nonceHeader);
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,295 @@
|
||||||
|
/*
|
||||||
|
* acme4j - Java ACME client
|
||||||
|
*
|
||||||
|
* Copyright (C) 2015 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.impl;
|
||||||
|
|
||||||
|
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.URI;
|
||||||
|
import java.net.URISyntaxException;
|
||||||
|
import java.security.KeyPair;
|
||||||
|
import java.security.cert.CertificateException;
|
||||||
|
import java.security.cert.CertificateFactory;
|
||||||
|
import java.security.cert.X509Certificate;
|
||||||
|
import java.util.EnumMap;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.regex.Pattern;
|
||||||
|
|
||||||
|
import org.jose4j.base64url.Base64Url;
|
||||||
|
import org.jose4j.json.JsonUtil;
|
||||||
|
import org.jose4j.jwk.JsonWebKey;
|
||||||
|
import org.jose4j.jws.AlgorithmIdentifiers;
|
||||||
|
import org.jose4j.jws.JsonWebSignature;
|
||||||
|
import org.jose4j.lang.JoseException;
|
||||||
|
import org.shredzone.acme4j.Account;
|
||||||
|
import org.shredzone.acme4j.connector.Connection;
|
||||||
|
import org.shredzone.acme4j.connector.HttpConnector;
|
||||||
|
import org.shredzone.acme4j.connector.Resource;
|
||||||
|
import org.shredzone.acme4j.connector.Session;
|
||||||
|
import org.shredzone.acme4j.exception.AcmeException;
|
||||||
|
import org.shredzone.acme4j.exception.AcmeServerException;
|
||||||
|
import org.shredzone.acme4j.util.ClaimBuilder;
|
||||||
|
import org.slf4j.Logger;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Default implementation of {@link Connection}.
|
||||||
|
*
|
||||||
|
* @author Richard "Shred" Körber
|
||||||
|
*/
|
||||||
|
public class DefaultConnection implements Connection {
|
||||||
|
private static final Logger LOG = LoggerFactory.getLogger(DefaultConnection.class);
|
||||||
|
|
||||||
|
private static final Pattern BASE64URL_PATTERN = Pattern.compile("[0-9A-Za-z_-]+");
|
||||||
|
|
||||||
|
protected final HttpConnector httpConnector;
|
||||||
|
protected HttpURLConnection conn;
|
||||||
|
|
||||||
|
public DefaultConnection(HttpConnector httpConnector) {
|
||||||
|
this.httpConnector = httpConnector;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void startSession(URI uri, Session session) throws AcmeException {
|
||||||
|
try {
|
||||||
|
LOG.debug("Initial replay nonce from {}", uri);
|
||||||
|
HttpURLConnection localConn = httpConnector.openConnection(uri);
|
||||||
|
localConn.setRequestMethod("HEAD");
|
||||||
|
localConn.connect();
|
||||||
|
|
||||||
|
session.setNonce(getNonceFromHeader(localConn));
|
||||||
|
} catch (IOException ex) {
|
||||||
|
throw new AcmeException("Failed to request a nonce", ex);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int sendRequest(URI uri) throws AcmeException {
|
||||||
|
try {
|
||||||
|
LOG.debug("GET {}", uri);
|
||||||
|
|
||||||
|
conn = httpConnector.openConnection(uri);
|
||||||
|
conn.setRequestMethod("GET");
|
||||||
|
conn.setRequestProperty("Accept-Charset", "utf-8");
|
||||||
|
conn.setDoOutput(false);
|
||||||
|
|
||||||
|
conn.connect();
|
||||||
|
|
||||||
|
throwException();
|
||||||
|
|
||||||
|
return conn.getResponseCode();
|
||||||
|
} catch (IOException ex) {
|
||||||
|
throw new AcmeException("API access failed", ex);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int sendSignedRequest(URI uri, ClaimBuilder claims, Session session, Account account) throws AcmeException {
|
||||||
|
try {
|
||||||
|
KeyPair keypair = account.getKeyPair();
|
||||||
|
|
||||||
|
if (session.getNonce() == null) {
|
||||||
|
startSession(uri, session);
|
||||||
|
}
|
||||||
|
if (session.getNonce() == null) {
|
||||||
|
throw new AcmeException("No nonce available");
|
||||||
|
}
|
||||||
|
|
||||||
|
LOG.debug("POST {} with claims: {}", uri, claims);
|
||||||
|
|
||||||
|
conn = httpConnector.openConnection(uri);
|
||||||
|
conn.setRequestMethod("POST");
|
||||||
|
conn.setRequestProperty("Accept", "application/json");
|
||||||
|
conn.setRequestProperty("Accept-Charset", "utf-8");
|
||||||
|
conn.setRequestProperty("Content-Type", "application/json");
|
||||||
|
conn.setDoOutput(true);
|
||||||
|
|
||||||
|
final JsonWebKey jwk = JsonWebKey.Factory.newJwk(keypair.getPublic());
|
||||||
|
|
||||||
|
JsonWebSignature jws = new JsonWebSignature();
|
||||||
|
jws.setPayload(claims.toString());
|
||||||
|
jws.getHeaders().setObjectHeaderValue("nonce", Base64Url.encode(session.getNonce()));
|
||||||
|
jws.getHeaders().setJwkHeaderValue("jwk", jwk);
|
||||||
|
jws.setAlgorithmHeaderValue(AlgorithmIdentifiers.RSA_USING_SHA256);
|
||||||
|
jws.setKey(keypair.getPrivate());
|
||||||
|
byte[] outputData = jws.getCompactSerialization().getBytes("utf-8");
|
||||||
|
|
||||||
|
conn.setFixedLengthStreamingMode(outputData.length);
|
||||||
|
conn.connect();
|
||||||
|
|
||||||
|
try (OutputStream out = conn.getOutputStream()) {
|
||||||
|
out.write(outputData);
|
||||||
|
}
|
||||||
|
|
||||||
|
session.setNonce(getNonceFromHeader(conn));
|
||||||
|
|
||||||
|
throwException();
|
||||||
|
|
||||||
|
return conn.getResponseCode();
|
||||||
|
} catch (JoseException | IOException ex) {
|
||||||
|
throw new AcmeException("Failed to send request to " + uri, ex);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Map<String, Object> readJsonResponse() throws AcmeException {
|
||||||
|
if (conn == null) {
|
||||||
|
throw new IllegalStateException("Not connected");
|
||||||
|
}
|
||||||
|
|
||||||
|
String contentType = conn.getHeaderField("Content-Type");
|
||||||
|
if (!("application/json".equals(contentType)
|
||||||
|
|| "application/problem+json".equals(contentType))) {
|
||||||
|
throw new AcmeException("Unexpected content type: " + contentType);
|
||||||
|
}
|
||||||
|
|
||||||
|
StringBuilder sb = new StringBuilder();
|
||||||
|
Map<String, Object> result = null;
|
||||||
|
|
||||||
|
try {
|
||||||
|
InputStream in = (conn.getResponseCode() < 400 ? conn.getInputStream() : conn.getErrorStream());
|
||||||
|
if (in != null) {
|
||||||
|
try (BufferedReader r = new BufferedReader(new InputStreamReader(in, "utf-8"))) {
|
||||||
|
sb.append(r.readLine());
|
||||||
|
}
|
||||||
|
|
||||||
|
result = JsonUtil.parseJson(sb.toString());
|
||||||
|
LOG.debug("Result JSON: {}", sb);
|
||||||
|
}
|
||||||
|
|
||||||
|
} catch (JoseException | IOException ex) {
|
||||||
|
throw new AcmeException("Failed to parse response: " + sb, ex);
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public X509Certificate readCertificate() throws AcmeException {
|
||||||
|
if (conn == null) {
|
||||||
|
throw new IllegalStateException("Not connected");
|
||||||
|
}
|
||||||
|
|
||||||
|
String contentType = conn.getHeaderField("Content-Type");
|
||||||
|
if (!("application/pkix-cert".equals(contentType))) {
|
||||||
|
throw new AcmeException("Unexpected content type: " + contentType);
|
||||||
|
}
|
||||||
|
|
||||||
|
try (InputStream in = conn.getInputStream()) {
|
||||||
|
CertificateFactory cf = CertificateFactory.getInstance("X.509");
|
||||||
|
return (X509Certificate) cf.generateCertificate(in);
|
||||||
|
} catch (IOException ex) {
|
||||||
|
throw new AcmeException("Failed to read certificate", ex);
|
||||||
|
} catch (CertificateException ex) {
|
||||||
|
throw new AcmeException("Error while generating the X.509 certificate", ex);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Map<Resource, URI> readDirectory() throws AcmeException {
|
||||||
|
String contentType = conn.getHeaderField("Content-Type");
|
||||||
|
if (!("application/json".equals(contentType))) {
|
||||||
|
throw new AcmeException("Unexpected content type: " + contentType);
|
||||||
|
}
|
||||||
|
|
||||||
|
EnumMap<Resource, URI> resourceMap = new EnumMap<>(Resource.class);
|
||||||
|
|
||||||
|
StringBuilder sb = new StringBuilder();
|
||||||
|
try (BufferedReader reader = new BufferedReader(new InputStreamReader(conn.getInputStream(), "utf-8"))) {
|
||||||
|
sb.append(reader.readLine());
|
||||||
|
} catch (IOException ex) {
|
||||||
|
throw new AcmeException("Could not read resource map", ex);
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
Map<String, Object> result = JsonUtil.parseJson(sb.toString());
|
||||||
|
for (Map.Entry<String, Object> entry : result.entrySet()) {
|
||||||
|
Resource res = Resource.parse(entry.getKey());
|
||||||
|
if (res != null) {
|
||||||
|
URI uri = new URI(entry.getValue().toString());
|
||||||
|
resourceMap.put(res, uri);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
LOG.debug("Resource directory: {}", resourceMap);
|
||||||
|
} catch (JoseException | URISyntaxException ex) {
|
||||||
|
throw new AcmeException("Could not parse resource map: " + sb, ex);
|
||||||
|
}
|
||||||
|
|
||||||
|
return resourceMap;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public URI getLocation() throws AcmeException {
|
||||||
|
String location = conn.getHeaderField("Location");
|
||||||
|
if (location == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
LOG.debug("Location: {}", location);
|
||||||
|
return new URI(location);
|
||||||
|
} catch (URISyntaxException ex) {
|
||||||
|
throw new AcmeException("Bad Location header: " + location);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void close() {
|
||||||
|
conn = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks if the server returned an error, and if so, throws a {@link AcmeException}.
|
||||||
|
*
|
||||||
|
* @throws AcmeException
|
||||||
|
* if the server returned a JSON problem
|
||||||
|
*/
|
||||||
|
protected void throwException() throws AcmeException {
|
||||||
|
if ("application/problem+json".equals(conn.getHeaderField("Content-Type"))) {
|
||||||
|
Map<String, Object> map = readJsonResponse();
|
||||||
|
String type = (String) map.get("type");
|
||||||
|
String detail = (String) map.get("detail");
|
||||||
|
throw new AcmeServerException(type, detail);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Extracts a nonce from the header.
|
||||||
|
*
|
||||||
|
* @param localConn
|
||||||
|
* {@link HttpURLConnection} to get the nonce from
|
||||||
|
* @return Nonce
|
||||||
|
* @throws AcmeException
|
||||||
|
* if there was no {@code Replay-Nonce} header, or the nonce was invalid
|
||||||
|
*/
|
||||||
|
protected byte[] getNonceFromHeader(HttpURLConnection localConn) throws AcmeException {
|
||||||
|
String nonceHeader = localConn.getHeaderField("Replay-Nonce");
|
||||||
|
if (nonceHeader == null || nonceHeader.trim().isEmpty()) {
|
||||||
|
throw new AcmeException("No replay nonce");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!BASE64URL_PATTERN.matcher(nonceHeader).matches()) {
|
||||||
|
throw new AcmeException("Invalid replay nonce: " + nonceHeader);
|
||||||
|
}
|
||||||
|
|
||||||
|
LOG.debug("Replay Nonce: {}", nonceHeader);
|
||||||
|
|
||||||
|
return Base64Url.decode(nonceHeader);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -24,6 +24,7 @@ import org.shredzone.acme4j.challenge.ProofOfPossessionChallenge;
|
||||||
import org.shredzone.acme4j.challenge.TlsSniChallenge;
|
import org.shredzone.acme4j.challenge.TlsSniChallenge;
|
||||||
import org.shredzone.acme4j.connector.Connection;
|
import org.shredzone.acme4j.connector.Connection;
|
||||||
import org.shredzone.acme4j.connector.HttpConnector;
|
import org.shredzone.acme4j.connector.HttpConnector;
|
||||||
|
import org.shredzone.acme4j.impl.DefaultConnection;
|
||||||
import org.shredzone.acme4j.impl.GenericAcmeClient;
|
import org.shredzone.acme4j.impl.GenericAcmeClient;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -75,7 +76,7 @@ public abstract class AbstractAcmeClientProvider implements AcmeClientProvider {
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public Connection createConnection() {
|
public Connection createConnection() {
|
||||||
return new Connection(createHttpConnector());
|
return new DefaultConnection(createHttpConnector());
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -11,7 +11,7 @@
|
||||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
|
||||||
*/
|
*/
|
||||||
package org.shredzone.acme4j.connector;
|
package org.shredzone.acme4j.impl;
|
||||||
|
|
||||||
import static org.hamcrest.Matchers.*;
|
import static org.hamcrest.Matchers.*;
|
||||||
import static org.junit.Assert.*;
|
import static org.junit.Assert.*;
|
||||||
|
@ -39,6 +39,10 @@ import org.jose4j.jwx.CompactSerializer;
|
||||||
import org.junit.Before;
|
import org.junit.Before;
|
||||||
import org.junit.Test;
|
import org.junit.Test;
|
||||||
import org.shredzone.acme4j.Account;
|
import org.shredzone.acme4j.Account;
|
||||||
|
import org.shredzone.acme4j.connector.Connection;
|
||||||
|
import org.shredzone.acme4j.connector.HttpConnector;
|
||||||
|
import org.shredzone.acme4j.connector.Resource;
|
||||||
|
import org.shredzone.acme4j.connector.Session;
|
||||||
import org.shredzone.acme4j.exception.AcmeException;
|
import org.shredzone.acme4j.exception.AcmeException;
|
||||||
import org.shredzone.acme4j.exception.AcmeServerException;
|
import org.shredzone.acme4j.exception.AcmeServerException;
|
||||||
import org.shredzone.acme4j.util.ClaimBuilder;
|
import org.shredzone.acme4j.util.ClaimBuilder;
|
||||||
|
@ -49,7 +53,7 @@ import org.shredzone.acme4j.util.TestUtils;
|
||||||
*
|
*
|
||||||
* @author Richard "Shred" Körber
|
* @author Richard "Shred" Körber
|
||||||
*/
|
*/
|
||||||
public class ConnectionTest {
|
public class DefaultConnectionTest {
|
||||||
|
|
||||||
private URI requestUri;
|
private URI requestUri;
|
||||||
private HttpURLConnection mockUrlConnection;
|
private HttpURLConnection mockUrlConnection;
|
||||||
|
@ -73,7 +77,7 @@ public class ConnectionTest {
|
||||||
public void testNoNonceFromHeader() throws AcmeException {
|
public void testNoNonceFromHeader() throws AcmeException {
|
||||||
when(mockUrlConnection.getHeaderField("Replay-Nonce")).thenReturn(null);
|
when(mockUrlConnection.getHeaderField("Replay-Nonce")).thenReturn(null);
|
||||||
|
|
||||||
try (Connection conn = new Connection(mockHttpConnection)) {
|
try (DefaultConnection conn = new DefaultConnection(mockHttpConnection)) {
|
||||||
conn.getNonceFromHeader(mockUrlConnection);
|
conn.getNonceFromHeader(mockUrlConnection);
|
||||||
fail("Expected to fail");
|
fail("Expected to fail");
|
||||||
} catch (AcmeException ex) {
|
} catch (AcmeException ex) {
|
||||||
|
@ -95,7 +99,7 @@ public class ConnectionTest {
|
||||||
when(mockUrlConnection.getHeaderField("Replay-Nonce"))
|
when(mockUrlConnection.getHeaderField("Replay-Nonce"))
|
||||||
.thenReturn(Base64Url.encode(nonce));
|
.thenReturn(Base64Url.encode(nonce));
|
||||||
|
|
||||||
try (Connection conn = new Connection(mockHttpConnection)) {
|
try (DefaultConnection conn = new DefaultConnection(mockHttpConnection)) {
|
||||||
byte[] nonceFromHeader = conn.getNonceFromHeader(mockUrlConnection);
|
byte[] nonceFromHeader = conn.getNonceFromHeader(mockUrlConnection);
|
||||||
assertThat(nonceFromHeader, is(nonce));
|
assertThat(nonceFromHeader, is(nonce));
|
||||||
}
|
}
|
||||||
|
@ -114,7 +118,7 @@ public class ConnectionTest {
|
||||||
|
|
||||||
when(mockUrlConnection.getHeaderField("Replay-Nonce")).thenReturn(badNonce);
|
when(mockUrlConnection.getHeaderField("Replay-Nonce")).thenReturn(badNonce);
|
||||||
|
|
||||||
try (Connection conn = new Connection(mockHttpConnection)) {
|
try (DefaultConnection conn = new DefaultConnection(mockHttpConnection)) {
|
||||||
conn.getNonceFromHeader(mockUrlConnection);
|
conn.getNonceFromHeader(mockUrlConnection);
|
||||||
fail("Expected to fail");
|
fail("Expected to fail");
|
||||||
} catch (AcmeException ex) {
|
} catch (AcmeException ex) {
|
||||||
|
@ -132,7 +136,7 @@ public class ConnectionTest {
|
||||||
public void testGetLocation() throws Exception {
|
public void testGetLocation() throws Exception {
|
||||||
when(mockUrlConnection.getHeaderField("Location")).thenReturn("http://example.com/otherlocation");
|
when(mockUrlConnection.getHeaderField("Location")).thenReturn("http://example.com/otherlocation");
|
||||||
|
|
||||||
try (Connection conn = new Connection(mockHttpConnection)) {
|
try (DefaultConnection conn = new DefaultConnection(mockHttpConnection)) {
|
||||||
conn.conn = mockUrlConnection;
|
conn.conn = mockUrlConnection;
|
||||||
URI location = conn.getLocation();
|
URI location = conn.getLocation();
|
||||||
assertThat(location, is(new URI("http://example.com/otherlocation")));
|
assertThat(location, is(new URI("http://example.com/otherlocation")));
|
||||||
|
@ -147,7 +151,7 @@ public class ConnectionTest {
|
||||||
*/
|
*/
|
||||||
@Test
|
@Test
|
||||||
public void testNoLocation() throws Exception {
|
public void testNoLocation() throws Exception {
|
||||||
try (Connection conn = new Connection(mockHttpConnection)) {
|
try (DefaultConnection conn = new DefaultConnection(mockHttpConnection)) {
|
||||||
conn.conn = mockUrlConnection;
|
conn.conn = mockUrlConnection;
|
||||||
URI location = conn.getLocation();
|
URI location = conn.getLocation();
|
||||||
assertThat(location, is(nullValue()));
|
assertThat(location, is(nullValue()));
|
||||||
|
@ -164,7 +168,7 @@ public class ConnectionTest {
|
||||||
public void testNoThrowException() throws AcmeException {
|
public void testNoThrowException() throws AcmeException {
|
||||||
when(mockUrlConnection.getHeaderField("Content-Type")).thenReturn("application/json");
|
when(mockUrlConnection.getHeaderField("Content-Type")).thenReturn("application/json");
|
||||||
|
|
||||||
try (Connection conn = new Connection(mockHttpConnection)) {
|
try (DefaultConnection conn = new DefaultConnection(mockHttpConnection)) {
|
||||||
conn.conn = mockUrlConnection;
|
conn.conn = mockUrlConnection;
|
||||||
conn.throwException();
|
conn.throwException();
|
||||||
}
|
}
|
||||||
|
@ -184,7 +188,7 @@ public class ConnectionTest {
|
||||||
when(mockUrlConnection.getResponseCode()).thenReturn(HttpURLConnection.HTTP_FORBIDDEN);
|
when(mockUrlConnection.getResponseCode()).thenReturn(HttpURLConnection.HTTP_FORBIDDEN);
|
||||||
when(mockUrlConnection.getErrorStream()).thenReturn(new ByteArrayInputStream(jsonData.getBytes("utf-8")));
|
when(mockUrlConnection.getErrorStream()).thenReturn(new ByteArrayInputStream(jsonData.getBytes("utf-8")));
|
||||||
|
|
||||||
try (Connection conn = new Connection(mockHttpConnection)) {
|
try (DefaultConnection conn = new DefaultConnection(mockHttpConnection)) {
|
||||||
conn.conn = mockUrlConnection;
|
conn.conn = mockUrlConnection;
|
||||||
conn.throwException();
|
conn.throwException();
|
||||||
fail("Expected to fail");
|
fail("Expected to fail");
|
||||||
|
@ -210,7 +214,7 @@ public class ConnectionTest {
|
||||||
when(mockUrlConnection.getHeaderField("Content-Type"))
|
when(mockUrlConnection.getHeaderField("Content-Type"))
|
||||||
.thenReturn("application/problem+json");
|
.thenReturn("application/problem+json");
|
||||||
|
|
||||||
try (Connection conn = new Connection(mockHttpConnection) {
|
try (DefaultConnection conn = new DefaultConnection(mockHttpConnection) {
|
||||||
@Override
|
@Override
|
||||||
public Map<String,Object> readJsonResponse() throws AcmeException {
|
public Map<String,Object> readJsonResponse() throws AcmeException {
|
||||||
Map<String, Object> result = new HashMap<String, Object>();
|
Map<String, Object> result = new HashMap<String, Object>();
|
||||||
|
@ -245,7 +249,7 @@ public class ConnectionTest {
|
||||||
.thenReturn(Base64Url.encode(nonce));
|
.thenReturn(Base64Url.encode(nonce));
|
||||||
|
|
||||||
Session session = new Session();
|
Session session = new Session();
|
||||||
try (Connection conn = new Connection(mockHttpConnection)) {
|
try (DefaultConnection conn = new DefaultConnection(mockHttpConnection)) {
|
||||||
conn.startSession(requestUri, session);
|
conn.startSession(requestUri, session);
|
||||||
}
|
}
|
||||||
assertThat(session.getNonce(), is(nonce));
|
assertThat(session.getNonce(), is(nonce));
|
||||||
|
@ -263,7 +267,7 @@ public class ConnectionTest {
|
||||||
public void testSendRequest() throws Exception {
|
public void testSendRequest() throws Exception {
|
||||||
final Set<String> invoked = new HashSet<>();
|
final Set<String> invoked = new HashSet<>();
|
||||||
|
|
||||||
try (Connection conn = new Connection(mockHttpConnection) {
|
try (DefaultConnection conn = new DefaultConnection(mockHttpConnection) {
|
||||||
@Override
|
@Override
|
||||||
protected void throwException() throws AcmeException {
|
protected void throwException() throws AcmeException {
|
||||||
invoked.add("throwException");
|
invoked.add("throwException");
|
||||||
|
@ -295,7 +299,7 @@ public class ConnectionTest {
|
||||||
when(mockUrlConnection.getOutputStream()).thenReturn(outputStream);
|
when(mockUrlConnection.getOutputStream()).thenReturn(outputStream);
|
||||||
when(mockUrlConnection.getHeaderField("Replay-Nonce")).thenReturn(Base64Url.encode(nonce2));
|
when(mockUrlConnection.getHeaderField("Replay-Nonce")).thenReturn(Base64Url.encode(nonce2));
|
||||||
|
|
||||||
try (Connection conn = new Connection(mockHttpConnection) {
|
try (DefaultConnection conn = new DefaultConnection(mockHttpConnection) {
|
||||||
@Override
|
@Override
|
||||||
protected void throwException() throws AcmeException {
|
protected void throwException() throws AcmeException {
|
||||||
invoked.add("throwException");
|
invoked.add("throwException");
|
||||||
|
@ -363,7 +367,7 @@ public class ConnectionTest {
|
||||||
when(mockUrlConnection.getResponseCode()).thenReturn(HttpURLConnection.HTTP_OK);
|
when(mockUrlConnection.getResponseCode()).thenReturn(HttpURLConnection.HTTP_OK);
|
||||||
when(mockUrlConnection.getInputStream()).thenReturn(new ByteArrayInputStream(jsonData.getBytes("utf-8")));
|
when(mockUrlConnection.getInputStream()).thenReturn(new ByteArrayInputStream(jsonData.getBytes("utf-8")));
|
||||||
|
|
||||||
try (Connection conn = new Connection(mockHttpConnection)) {
|
try (DefaultConnection conn = new DefaultConnection(mockHttpConnection)) {
|
||||||
conn.conn = mockUrlConnection;
|
conn.conn = mockUrlConnection;
|
||||||
Map<String, Object> result = conn.readJsonResponse();
|
Map<String, Object> result = conn.readJsonResponse();
|
||||||
assertThat(result.keySet(), hasSize(2));
|
assertThat(result.keySet(), hasSize(2));
|
||||||
|
@ -392,7 +396,7 @@ public class ConnectionTest {
|
||||||
when(mockUrlConnection.getInputStream()).thenReturn(new ByteArrayInputStream(original.getEncoded()));
|
when(mockUrlConnection.getInputStream()).thenReturn(new ByteArrayInputStream(original.getEncoded()));
|
||||||
|
|
||||||
X509Certificate downloaded;
|
X509Certificate downloaded;
|
||||||
try (Connection conn = new Connection(mockHttpConnection)) {
|
try (DefaultConnection conn = new DefaultConnection(mockHttpConnection)) {
|
||||||
conn.conn = mockUrlConnection;
|
conn.conn = mockUrlConnection;
|
||||||
downloaded = conn.readCertificate();
|
downloaded = conn.readCertificate();
|
||||||
}
|
}
|
||||||
|
@ -421,7 +425,7 @@ public class ConnectionTest {
|
||||||
when(mockUrlConnection.getHeaderField("Content-Type")).thenReturn("application/json");
|
when(mockUrlConnection.getHeaderField("Content-Type")).thenReturn("application/json");
|
||||||
when(mockUrlConnection.getInputStream()).thenReturn(new ByteArrayInputStream(jsonData.toString().getBytes("utf-8")));
|
when(mockUrlConnection.getInputStream()).thenReturn(new ByteArrayInputStream(jsonData.toString().getBytes("utf-8")));
|
||||||
|
|
||||||
try (Connection conn = new Connection(mockHttpConnection)) {
|
try (DefaultConnection conn = new DefaultConnection(mockHttpConnection)) {
|
||||||
conn.conn = mockUrlConnection;
|
conn.conn = mockUrlConnection;
|
||||||
Map<Resource, URI> result = conn.readDirectory();
|
Map<Resource, URI> result = conn.readDirectory();
|
||||||
assertThat(result.keySet(), hasSize(2));
|
assertThat(result.keySet(), hasSize(2));
|
Loading…
Reference in New Issue