diff --git a/acme4j-it/pom.xml b/acme4j-it/pom.xml index d016ecca..94b99180 100644 --- a/acme4j-it/pom.xml +++ b/acme4j-it/pom.xml @@ -74,7 +74,7 @@ io.fabric8 docker-maven-plugin - 0.20.1 + 0.21.0 true @@ -87,7 +87,7 @@ acme4j/pebble:${project.version} - golang:1.7 + golang:latest true go get -u -v -d gopkg.in/square/go-jose.v2 @@ -100,7 +100,10 @@ 14000 - pebble -config /etc/pebble/pebble-config.json + + echo "nameserver $(grep 'bammbamm' /etc/hosts|cut -f1)">/etc/resolv.conf; \ + pebble -config /etc/pebble/pebble-config.json + dir @@ -115,12 +118,58 @@ - - host - - - example.com:127.0.0.1 - + + 14000:14000 + + + bammbamm + + + Pebble running + + + + + + + bammbamm + acme4j/bammbamm:${project.version} + + + openjdk:8-jre + + 53/udp + 5001 + 5002 + 14001 + + + + java -cp "/maven/*" org.shredzone.acme4j.it.BammBamm + + + + artifact-with-dependencies + + + + + bammbamm + + 14001:14001 + + + Bammbamm running + @@ -159,11 +208,21 @@ nanohttpd ${nanohttpd.version} + + org.nanohttpd + nanohttpd-nanolets + ${nanohttpd.version} + dnsjava dnsjava ${dnsjava.version} + + org.apache.httpcomponents + httpclient + ${httpclient.version} + org.slf4j diff --git a/acme4j-it/src/main/java/org/shredzone/acme4j/it/AbstractResponder.java b/acme4j-it/src/main/java/org/shredzone/acme4j/it/AbstractResponder.java new file mode 100644 index 00000000..aa161273 --- /dev/null +++ b/acme4j-it/src/main/java/org/shredzone/acme4j/it/AbstractResponder.java @@ -0,0 +1,78 @@ +/* + * acme4j - Java ACME client + * + * Copyright (C) 2017 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.it; + +import java.util.HashMap; +import java.util.Map; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import fi.iki.elonen.NanoHTTPD; +import fi.iki.elonen.NanoHTTPD.IHTTPSession; +import fi.iki.elonen.NanoHTTPD.Response; +import fi.iki.elonen.router.RouterNanoHTTPD.UriResource; +import fi.iki.elonen.router.RouterNanoHTTPD.UriResponder; + +/** + * A generic responder class for requests. + */ +public abstract class AbstractResponder implements UriResponder { + private static final Logger LOG = LoggerFactory.getLogger(AbstractResponder.class); + + /** + * Handles the request. + * + * @param urlParams + * Map of decoded URL parameters + * @param session + * {@link IHTTPSession} containing the decoding body parameters + */ + public abstract void handle(Map urlParams, IHTTPSession session) throws Exception; + + @Override + public Response post(UriResource uriResource, Map urlParams, IHTTPSession session) { + LOG.info("POST " + uriResource); + try { + session.parseBody(new HashMap<>()); + handle(urlParams, session); + return NanoHTTPD.newFixedLengthResponse(Response.Status.OK, NanoHTTPD.MIME_PLAINTEXT, "OK"); + } catch (Exception ex) { + LOG.error("Request failed", ex); + return NanoHTTPD.newFixedLengthResponse(Response.Status.INTERNAL_ERROR, NanoHTTPD.MIME_PLAINTEXT, ex.toString()); + } + } + + @Override + public Response get(UriResource uriResource, Map urlParams, IHTTPSession session) { + LOG.warn("Unsupported " + session.getMethod() + " " + uriResource); + return NanoHTTPD.newFixedLengthResponse(Response.Status.FORBIDDEN, NanoHTTPD.MIME_PLAINTEXT, "use POST"); + } + + @Override + public Response put(UriResource uriResource, Map urlParams, IHTTPSession session) { + return get(uriResource, urlParams, session); + } + + @Override + public Response delete(UriResource uriResource, Map urlParams, IHTTPSession session) { + return get(uriResource, urlParams, session); + } + + @Override + public Response other(String method, UriResource uriResource, Map urlParams, IHTTPSession session) { + return get(uriResource, urlParams, session); + } + +} diff --git a/acme4j-it/src/main/java/org/shredzone/acme4j/it/BammBamm.java b/acme4j-it/src/main/java/org/shredzone/acme4j/it/BammBamm.java new file mode 100644 index 00000000..cc04fd8d --- /dev/null +++ b/acme4j-it/src/main/java/org/shredzone/acme4j/it/BammBamm.java @@ -0,0 +1,148 @@ +/* + * acme4j - Java ACME client + * + * Copyright (C) 2017 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.it; + +import java.io.IOException; +import java.io.UncheckedIOException; +import java.util.ResourceBundle; + +import org.shredzone.acme4j.it.server.DnsServer; +import org.shredzone.acme4j.it.server.HttpServer; +import org.shredzone.acme4j.it.server.TlsSniServer; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import fi.iki.elonen.NanoHTTPD; +import fi.iki.elonen.router.RouterNanoHTTPD; + +/** + * A mock server to test Pebble. It provides a HTTP server, TLS-SNI server, and DNS + * server. The servers can be configured remotely via simple HTTP POST requests. + *

+ * WARNING: This is a very simple server that is only meant to be used for + * integration tests. Do not use in the outside world! + */ +public class BammBamm { + private static final Logger LOG = LoggerFactory.getLogger(BammBamm.class); + + private static final BammBamm INSTANCE = new BammBamm(); + + private final int appPort; + private final int httpPort; + private final int dnsPort; + private final int tlsSniPort; + private final AppServer appServer; + private final DnsServer dnsServer; + private final HttpServer httpServer; + private final TlsSniServer tlsSniServer; + + /** + * Retrieves the singleton instance of {@link BammBamm}. + */ + public static BammBamm instance() { + return INSTANCE; + } + + private BammBamm() { + ResourceBundle bundle = ResourceBundle.getBundle("bammbamm"); + appPort = Integer.parseInt(bundle.getString("app.port")); + dnsPort = Integer.parseInt(bundle.getString("dns.port")); + httpPort = Integer.parseInt(bundle.getString("http.port")); + tlsSniPort = Integer.parseInt(bundle.getString("tlsSni.port")); + + dnsServer = new DnsServer(); + httpServer = new HttpServer(); + tlsSniServer = new TlsSniServer(); + appServer = new AppServer(appPort); + } + + /** + * Returns the {@link DnsServer} instance. + */ + public DnsServer getDnsServer() { + return dnsServer; + } + + /** + * Returns the {@link HttpServer} instance. + */ + public HttpServer getHttpServer() { + return httpServer; + } + + /** + * Returns the {@link TlsSniServer} instance. + */ + public TlsSniServer getTlsSniServer() { + return tlsSniServer; + } + + /** + * Starts the servers. + */ + public void start() { + dnsServer.start(dnsPort); + httpServer.start(httpPort); + tlsSniServer.start(tlsSniPort); + + try { + appServer.start(NanoHTTPD.SOCKET_READ_TIMEOUT, true); + } catch (IOException ex) { + throw new UncheckedIOException(ex); + } + + LOG.info("Bammbamm running, listening on port {}", appServer.getListeningPort()); + } + + /** + * Stops the servers. + */ + public void stop() { + appServer.stop(); + tlsSniServer.stop(); + httpServer.stop(); + dnsServer.stop(); + + LOG.info("Bammbamm was stopped."); + } + + /** + * App server with all predefined routes. + */ + private static class AppServer extends RouterNanoHTTPD { + public AppServer(int port) { + super(port); + super.addMappings(); + + addRoute(DnsHandler.ADD_A_RECORD, DnsHandler.AddARecord.class); + addRoute(DnsHandler.REMOVE_A_RECORD, DnsHandler.RemoveARecord.class); + addRoute(DnsHandler.ADD_TXT_RECORD, DnsHandler.AddTxtRecord.class); + addRoute(DnsHandler.REMOVE_TXT_RECORD, DnsHandler.RemoveTxtRecord.class); + + addRoute(HttpHandler.ADD, HttpHandler.Add.class); + addRoute(HttpHandler.REMOVE, HttpHandler.Remove.class); + + addRoute(TlsSniHandler.ADD, TlsSniHandler.Add.class); + addRoute(TlsSniHandler.REMOVE, TlsSniHandler.Remove.class); + } + } + + /** + * Start bammbamm. It runs until the Java process is stopped. + */ + public static void main(String[] args) { + BammBamm.instance().start(); + } + +} diff --git a/acme4j-it/src/main/java/org/shredzone/acme4j/it/BammBammClient.java b/acme4j-it/src/main/java/org/shredzone/acme4j/it/BammBammClient.java new file mode 100644 index 00000000..38803d4a --- /dev/null +++ b/acme4j-it/src/main/java/org/shredzone/acme4j/it/BammBammClient.java @@ -0,0 +1,277 @@ +/* + * acme4j - Java ACME client + * + * Copyright (C) 2017 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.it; + +import java.io.IOException; +import java.io.UnsupportedEncodingException; +import java.net.URLEncoder; +import java.nio.charset.Charset; +import java.security.PrivateKey; +import java.security.cert.CertificateEncodingException; +import java.security.cert.X509Certificate; +import java.util.ArrayList; +import java.util.Base64; +import java.util.Base64.Encoder; +import java.util.List; + +import org.apache.http.HttpResponse; +import org.apache.http.HttpStatus; +import org.apache.http.NameValuePair; +import org.apache.http.client.ClientProtocolException; +import org.apache.http.client.HttpClient; +import org.apache.http.client.entity.UrlEncodedFormEntity; +import org.apache.http.client.methods.HttpPost; +import org.apache.http.impl.client.HttpClients; +import org.apache.http.message.BasicNameValuePair; +import org.apache.http.util.EntityUtils; + +/** + * A BammBamm client. + */ +public class BammBammClient { + private final String baseUrl; + + /** + * Creates a new BammBamm client. + * + * @param baseUrl + * Base URL of the BammBamm server to connect to. + */ + public BammBammClient(String baseUrl) { + this.baseUrl = baseUrl; + } + + /** + * Adds a HTTP token. + * + * @param token + * Token to add + * @param challenge + * Challenge to respond with + */ + public void httpAddToken(String token, String challenge) throws IOException { + createRequest(HttpHandler.ADD) + .arg(":token", token) + .param("challenge", challenge) + .submit(); + } + + /** + * Removes a HTTP token. + * + * @param token + * Token to remove + */ + public void httpRemoveToken(String token) throws IOException { + createRequest(HttpHandler.REMOVE) + .arg(":token", token) + .submit(); + } + + /** + * Adds an A Record to the DNS. Only one A Record is supported per domain. If another + * A Record is set, it will replace the existing one. + * + * @param domain + * Domain of the A Record + * @param ip + * IP address or domain name. If a domain name is used, it will be resolved + * and the IP will be used. + */ + public void dnsAddARecord(String domain, String ip) throws IOException { + createRequest(DnsHandler.ADD_A_RECORD) + .arg(":domain", domain) + .param("ip", ip) + .submit(); + } + + /** + * Removes an A Record from the DNS. + * + * @param domain + * Domain to remove the A Record from + */ + public void dnsRemoveARecord(String domain) throws IOException { + createRequest(DnsHandler.REMOVE_A_RECORD) + .arg(":domain", domain) + .submit(); + } + + /** + * Adds a TXT Record to the DNS. Only one TXT Record is supported per domain. If + * another TXT Record is set, it will replace the existing one. + * + * @param domain + * Domain to add the TXT Record to + * @param txt + * TXT record to add + */ + public void dnsAddTxtRecord(String domain, String txt) throws IOException { + createRequest(DnsHandler.ADD_TXT_RECORD) + .arg(":domain", domain) + .param("txt", txt) + .submit(); + } + + /** + * Removes a TXT Record from the DNS. + * + * @param domain + * Domain to remove the TXT Record from + */ + public void dnsRemoveTxtRecord(String domain) throws IOException { + createRequest(DnsHandler.REMOVE_TXT_RECORD) + .arg(":domain", domain) + .submit(); + } + + /** + * Adds a certificate for TLS-SNI tests. + * + * @param alias + * An alias to be used for removal, for example the domain name being + * validated. + * @param privateKey + * {@link PrivateKey} of the certificate + * @param cert + * {@link X509Certificate} containing the domain names to respond to + */ + public void tlsSniAddCertificate(String alias, PrivateKey privateKey, X509Certificate cert) throws IOException { + try { + createRequest(TlsSniHandler.ADD) + .arg(":alias", alias) + .param("privateKey", privateKey.getEncoded()) + .param("cert", cert.getEncoded()) + .submit(); + } catch (CertificateEncodingException ex) { + throw new IOException(ex); + } + } + + /** + * Removes a certificate. + * + * @param alias + * Certificate alias to remove + */ + public void tlsSniRemoveCertificate(String alias) throws IOException { + createRequest(TlsSniHandler.REMOVE) + .arg(":alias", alias) + .submit(); + } + + /** + * Creates a new {@link Request} object. + * + * @param call + * Path to be called + * @return Created {@link Request} object + */ + private Request createRequest(String call) { + return new Request(baseUrl, call); + } + + /** + * This class helps to assemble and invoke a HTTP POST request. + */ + private static class Request { + private static final HttpClient CLIENT = HttpClients.createDefault(); + private static final Encoder BASE64 = Base64.getEncoder(); + private static final Charset UTF8 = Charset.forName("utf-8"); + + private final List params = new ArrayList<>(); + private final String baseUrl; + private String call; + + /** + * Creates a new {@link Request}. + * + * @param baseUrl + * Base URL of the server to invoke + * @param call + * Path to invoke. It may contain placeholders. + */ + public Request(String baseUrl, String call) { + this.baseUrl = baseUrl; + this.call = call; + } + + /** + * Sets a path parameter. + * + * @param key + * Placeholder to change, leading ':' inclusive! + * @param value + * Value of the parameter + * @return itself + */ + public Request arg(String key, String value) { + try { + call = call.replace(key, URLEncoder.encode(value, UTF8.name())); + } catch (UnsupportedEncodingException ex) { + throw new InternalError("utf-8 missing", ex); + } + return this; + } + + /** + * Adds a form parameter. It will be sent in the request body. + * + * @param key + * Parameter name + * @param value + * Parameter value + * @return itself + */ + public Request param(String key, String value) { + params.add(new BasicNameValuePair(key, value)); + return this; + } + + /** + * Adds a binary form parameter. It will be sent in the request body. + * + * @param key + * Parameter name + * @param value + * Parameter value. It will be Base64 encoded. + * @return itself + */ + public Request param(String key, byte[] value) { + return param(key, BASE64.encodeToString(value)); + } + + /** + * Submits the POST request. + */ + public void submit() throws IOException { + try { + HttpPost httppost = new HttpPost(baseUrl + call); + if (!params.isEmpty()) { + httppost.setEntity(new UrlEncodedFormEntity(params, UTF8)); + } + HttpResponse response = CLIENT.execute(httppost); + + EntityUtils.consume(response.getEntity()); + + if (response.getStatusLine().getStatusCode() != HttpStatus.SC_OK) { + throw new IOException(response.getStatusLine().getReasonPhrase()); + } + } catch (ClientProtocolException ex) { + throw new IOException(ex); + } + } + } + +} diff --git a/acme4j-it/src/main/java/org/shredzone/acme4j/it/DnsHandler.java b/acme4j-it/src/main/java/org/shredzone/acme4j/it/DnsHandler.java new file mode 100644 index 00000000..59461baa --- /dev/null +++ b/acme4j-it/src/main/java/org/shredzone/acme4j/it/DnsHandler.java @@ -0,0 +1,75 @@ +/* + * acme4j - Java ACME client + * + * Copyright (C) 2017 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.it; + +import java.net.InetAddress; +import java.util.Map; + +import org.shredzone.acme4j.it.server.DnsServer; + +import fi.iki.elonen.NanoHTTPD.IHTTPSession; + +/** + * Request handler for all {@code dns-01} related requests. + */ +public class DnsHandler { + + public static final String ADD_A_RECORD = "/dns/add/a/:domain"; + public static final String REMOVE_A_RECORD = "/dns/remove/a/:domain"; + public static final String ADD_TXT_RECORD = "/dns/add/txt/:domain"; + public static final String REMOVE_TXT_RECORD = "/dns/remove/txt/:domain"; + + public static class AddARecord extends AbstractResponder { + @Override + public void handle(Map urlParams, IHTTPSession session) throws Exception { + String domain = urlParams.get("domain"); + String ip = session.getParameters().get("ip").get(0); + + DnsServer server = BammBamm.instance().getDnsServer(); + server.addARecord(domain, InetAddress.getByName(ip)); + } + } + + public static class RemoveARecord extends AbstractResponder { + @Override + public void handle(Map urlParams, IHTTPSession session) throws Exception { + String domain = urlParams.get("domain"); + + DnsServer server = BammBamm.instance().getDnsServer(); + server.removeARecord(domain); + } + } + + public static class AddTxtRecord extends AbstractResponder { + @Override + public void handle(Map urlParams, IHTTPSession session) throws Exception { + String domain = urlParams.get("domain"); + String txt = session.getParameters().get("txt").get(0); + + DnsServer server = BammBamm.instance().getDnsServer(); + server.addTxtRecord(domain, txt); + } + } + + public static class RemoveTxtRecord extends AbstractResponder { + @Override + public void handle(Map urlParams, IHTTPSession session) throws Exception { + String domain = urlParams.get("domain"); + + DnsServer server = BammBamm.instance().getDnsServer(); + server.removeTxtRecord(domain); + } + } + +} diff --git a/acme4j-it/src/main/java/org/shredzone/acme4j/it/HttpHandler.java b/acme4j-it/src/main/java/org/shredzone/acme4j/it/HttpHandler.java new file mode 100644 index 00000000..6bd64d20 --- /dev/null +++ b/acme4j-it/src/main/java/org/shredzone/acme4j/it/HttpHandler.java @@ -0,0 +1,51 @@ +/* + * acme4j - Java ACME client + * + * Copyright (C) 2017 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.it; + +import java.util.Map; + +import org.shredzone.acme4j.it.server.HttpServer; + +import fi.iki.elonen.NanoHTTPD.IHTTPSession; + +/** + * Request handler for all {@code http-01} related requests. + */ +public class HttpHandler { + + public static final String ADD = "/http/add/:token"; + public static final String REMOVE = "/http/remove/:token"; + + public static class Add extends AbstractResponder { + @Override + public void handle(Map urlParams, IHTTPSession session) throws Exception { + String token = urlParams.get("token"); + String challenge = session.getParameters().get("challenge").get(0); + + HttpServer server = BammBamm.instance().getHttpServer(); + server.addToken(token, challenge); + } + } + + public static class Remove extends AbstractResponder { + @Override + public void handle(Map urlParams, IHTTPSession session) throws Exception { + String token = urlParams.get("token"); + + HttpServer server = BammBamm.instance().getHttpServer(); + server.removeToken(token); + } + } + +} diff --git a/acme4j-it/src/main/java/org/shredzone/acme4j/it/TlsSniHandler.java b/acme4j-it/src/main/java/org/shredzone/acme4j/it/TlsSniHandler.java new file mode 100644 index 00000000..bb46b374 --- /dev/null +++ b/acme4j-it/src/main/java/org/shredzone/acme4j/it/TlsSniHandler.java @@ -0,0 +1,70 @@ +/* + * acme4j - Java ACME client + * + * Copyright (C) 2017 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.it; + +import java.io.ByteArrayInputStream; +import java.security.KeyFactory; +import java.security.PrivateKey; +import java.security.cert.CertificateFactory; +import java.security.cert.X509Certificate; +import java.security.spec.PKCS8EncodedKeySpec; +import java.util.Base64; +import java.util.Base64.Decoder; +import java.util.Map; + +import org.shredzone.acme4j.it.server.TlsSniServer; + +import fi.iki.elonen.NanoHTTPD.IHTTPSession; + +/** + * Request handler for all {@code tls-sni-02} related requests. + */ +public class TlsSniHandler { + + public static final String ADD = "/tlssni/add/:alias"; + public static final String REMOVE = "/tlssni/remove/:alias"; + + public static class Add extends AbstractResponder { + @Override + public void handle(Map urlParams, IHTTPSession session) throws Exception { + String alias = urlParams.get("alias"); + String privateKeyEncoded = session.getParameters().get("privateKey").get(0); + String certEncoded = session.getParameters().get("cert").get(0); + + Decoder base64 = Base64.getDecoder(); + + KeyFactory kf = KeyFactory.getInstance("RSA"); + PrivateKey privateKey = kf.generatePrivate(new PKCS8EncodedKeySpec( + base64.decode(privateKeyEncoded))); + + CertificateFactory cf = CertificateFactory.getInstance("X.509"); + X509Certificate cert = (X509Certificate) cf.generateCertificate( + new ByteArrayInputStream(base64.decode(certEncoded))); + + TlsSniServer server = BammBamm.instance().getTlsSniServer(); + server.addCertificate(alias, privateKey, cert); + } + } + + public static class Remove extends AbstractResponder { + @Override + public void handle(Map urlParams, IHTTPSession session) throws Exception { + String alias = urlParams.get("alias"); + + TlsSniServer server = BammBamm.instance().getTlsSniServer(); + server.removeCertificate(alias); + } + } + +} diff --git a/acme4j-it/src/main/resources/bammbamm.properties b/acme4j-it/src/main/resources/bammbamm.properties new file mode 100644 index 00000000..b05222c2 --- /dev/null +++ b/acme4j-it/src/main/resources/bammbamm.properties @@ -0,0 +1,12 @@ + +# Port where BammBamm is listening for new connections +app.port = 14001 + +# DNS server port +dns.port = 53 + +# HTTP server port +http.port = 5002 + +# TLS-SNI server port +tlsSni.port = 5001 diff --git a/acme4j-it/src/test/java/org/shredzone/acme4j/it/OrderIT.java b/acme4j-it/src/test/java/org/shredzone/acme4j/it/OrderIT.java index faaa39ed..9cc1693c 100644 --- a/acme4j-it/src/test/java/org/shredzone/acme4j/it/OrderIT.java +++ b/acme4j-it/src/test/java/org/shredzone/acme4j/it/OrderIT.java @@ -23,15 +23,12 @@ import java.security.cert.X509Certificate; import java.time.Duration; import java.time.Instant; -import org.junit.AfterClass; -import org.junit.BeforeClass; -import org.junit.Ignore; import org.junit.Test; +import org.shredzone.acme4j.Account; +import org.shredzone.acme4j.AccountBuilder; import org.shredzone.acme4j.Authorization; import org.shredzone.acme4j.Certificate; import org.shredzone.acme4j.Order; -import org.shredzone.acme4j.Account; -import org.shredzone.acme4j.AccountBuilder; import org.shredzone.acme4j.Session; import org.shredzone.acme4j.Status; import org.shredzone.acme4j.challenge.Challenge; @@ -40,9 +37,6 @@ import org.shredzone.acme4j.challenge.Http01Challenge; import org.shredzone.acme4j.challenge.TlsSni02Challenge; import org.shredzone.acme4j.exception.AcmeException; import org.shredzone.acme4j.exception.AcmeLazyLoadingException; -import org.shredzone.acme4j.it.server.DnsServer; -import org.shredzone.acme4j.it.server.HttpServer; -import org.shredzone.acme4j.it.server.TlsSniServer; import org.shredzone.acme4j.util.CSRBuilder; import org.shredzone.acme4j.util.CertificateUtils; @@ -51,38 +45,12 @@ import org.shredzone.acme4j.util.CertificateUtils; */ public class OrderIT extends PebbleITBase { - private static final int TLS_SNI_PORT = 5001; - private static final int HTTP_PORT = 5002; - private static final int DNS_PORT = 5003; - private static final String TEST_DOMAIN = "example.com"; - private static TlsSniServer tlsSniServer; - private static HttpServer httpServer; - private static DnsServer dnsServer; + private final String bammbammUrl = System.getProperty("bammbammUrl", "http://localhost:14001"); + private final String bammbammHostname = System.getProperty("bammbammHostname", "bammbamm"); - @BeforeClass - public static void setup() { - tlsSniServer = new TlsSniServer(); - tlsSniServer.start(TLS_SNI_PORT); - - httpServer = new HttpServer(); - httpServer.start(HTTP_PORT); - - dnsServer = new DnsServer(); - dnsServer.start(DNS_PORT); - - await().until(() -> tlsSniServer.isListening() - && httpServer.isListening() - && dnsServer.isListening()); - } - - @AfterClass - public static void shutdown() { - tlsSniServer.stop(); - httpServer.stop(); - dnsServer.stop(); - } + private BammBammClient client = new BammBammClient(bammbammUrl); /** * Test if a certificate can be ordered via tns-sni-02 challenge. @@ -98,7 +66,12 @@ public class OrderIT extends PebbleITBase { X509Certificate cert = CertificateUtils.createTlsSni02Certificate( challengeKey, challenge.getSubject(), challenge.getSanB()); - tlsSniServer.addCertificate(challenge.getSubject(), challengeKey.getPrivate(), cert); + client.dnsAddARecord(TEST_DOMAIN, bammbammHostname); + client.tlsSniAddCertificate(challenge.getSubject(), challengeKey.getPrivate(), cert); + + cleanup(() -> client.dnsRemoveARecord(TEST_DOMAIN)); + cleanup(() -> client.tlsSniRemoveCertificate(challenge.getSubject())); + return challenge; }); } @@ -112,7 +85,12 @@ public class OrderIT extends PebbleITBase { Http01Challenge challenge = auth.findChallenge(Http01Challenge.TYPE); assertThat(challenge, is(notNullValue())); - httpServer.addToken(challenge.getToken(), challenge.getAuthorization()); + client.dnsAddARecord(TEST_DOMAIN, bammbammHostname); + client.httpAddToken(challenge.getToken(), challenge.getAuthorization()); + + cleanup(() -> client.dnsRemoveARecord(TEST_DOMAIN)); + cleanup(() -> client.httpRemoveToken(challenge.getToken())); + return challenge; }); } @@ -121,13 +99,19 @@ public class OrderIT extends PebbleITBase { * Test if a certificate can be ordered via dns-01 challenge. */ @Test - @Ignore // TODO PEBBLE: cannot query our dnsServer yet... public void testDnsValidation() throws Exception { orderCertificate(TEST_DOMAIN, auth -> { Dns01Challenge challenge = auth.findChallenge(Dns01Challenge.TYPE); assertThat(challenge, is(notNullValue())); - dnsServer.addTxtRecord("_acme-challenge." + TEST_DOMAIN, challenge.getDigest()); + String challengeDomainName = "_acme-challenge." + TEST_DOMAIN; + + client.dnsAddARecord(TEST_DOMAIN, bammbammHostname); + client.dnsAddTxtRecord(challengeDomainName, challenge.getDigest()); + + cleanup(() -> client.dnsRemoveARecord(TEST_DOMAIN)); + cleanup(() -> client.dnsRemoveTxtRecord(challengeDomainName)); + return challenge; }); } @@ -173,7 +157,7 @@ public class OrderIT extends PebbleITBase { challenge.trigger(); await() - .pollInterval(3, SECONDS) + .pollInterval(1, SECONDS) .timeout(30, SECONDS) .conditionEvaluationListener(cond -> updateAuth(auth)) .until(auth::getStatus, not(Status.PENDING)); diff --git a/acme4j-it/src/test/java/org/shredzone/acme4j/it/PebbleITBase.java b/acme4j-it/src/test/java/org/shredzone/acme4j/it/PebbleITBase.java index 961b4d90..13a6074b 100644 --- a/acme4j-it/src/test/java/org/shredzone/acme4j/it/PebbleITBase.java +++ b/acme4j-it/src/test/java/org/shredzone/acme4j/it/PebbleITBase.java @@ -19,7 +19,10 @@ import static org.junit.Assert.assertThat; import java.net.URI; import java.net.URL; import java.security.KeyPair; +import java.util.ArrayList; +import java.util.List; +import org.junit.After; import org.shredzone.acme4j.util.KeyPairUtils; /** @@ -31,10 +34,23 @@ import org.shredzone.acme4j.util.KeyPairUtils; * {@code pebbleHost} and {@code pebblePort} respectively. */ public abstract class PebbleITBase { - private final String pebbleHost = System.getProperty("pebbleHost", "localhost"); private final int pebblePort = Integer.parseInt(System.getProperty("pebblePort", "14000")); + private final List cleanup = new ArrayList<>(); + + @After + public void performCleanup() throws Exception { + for (CleanupCallback callback : cleanup) { + callback.cleanup(); + } + cleanup.clear(); + } + + protected void cleanup(CleanupCallback callback) { + cleanup.add(callback); + } + /** * @return The {@link URI} of the pebble server to test against. */ @@ -66,4 +82,9 @@ public abstract class PebbleITBase { assertThat(url.getPath(), not(isEmptyOrNullString())); } + @FunctionalInterface + public static interface CleanupCallback { + void cleanup() throws Exception; + } + } diff --git a/pom.xml b/pom.xml index 63c38b3d..2b82bd79 100644 --- a/pom.xml +++ b/pom.xml @@ -53,6 +53,7 @@ 1.57 2.1.7 + 4.5.3 0.6.0 2.3.1 1.7.25 diff --git a/src/site/markdown/index.md.vm b/src/site/markdown/index.md.vm index 782b7d0b..3cc8264b 100644 --- a/src/site/markdown/index.md.vm +++ b/src/site/markdown/index.md.vm @@ -39,8 +39,10 @@ Now just have a look at [this source code](https://github.com/shred/acme4j/blob/ Running Integration Tests ------------------------- -_acme4j-client_ contains a number of integration tests. These tests are _not_ executed by maven by default, as they require a [Pebble ACME test server](https://github.com/letsencrypt/pebble). +_acme4j_ provides a number of integration tests. These tests are _not_ executed by maven by default, as they require Docker on the build machine. -To run them, install and run a Pebble server instance on your build machine. Then invoke maven with the `-DskipITs=false` option set, to enable the integration tests at `verify` phase. +To run them, install Docker and make it available to your user. Then invoke `mvn -Pci verify` to run the integration tests. The tests build images of the current [Pebble ACME test server](https://github.com/letsencrypt/pebble), and an internal test server that provides a configurable HTTP and DNS server for Pebble. -If you are running a Pebble server on a different host and/or port, invoke maven with the `pebbleHost` and `pebblePort` system properties set to the appropriate host and port, respectively. For example, to connect to `acme.example.com` port 12345, use `-DpebbleHost=acme.example.com -DpebblePort=12345`. +If you change into the `acme-it` project directory, you can also build, start and stop the test servers with `mvn docker:build`, `mvn docker:start` and `mvn docker:stop`, respectively. While the test servers are running, you can execute the integration tests in your IDE. + +The Pebble server requires a few workarounds in _acme4j_ at the moment. To enable them, pass `-Dpebble=true` as VM parameter when running the integration tests.