Move test servers into a separate Docker container

- Enables the dns-01 test
- Fixes integration test on MacOS
pull/55/head
Richard Körber 2017-07-30 16:01:41 +02:00
parent c5f5a6d3f5
commit 06985c4404
12 changed files with 833 additions and 55 deletions

View File

@ -74,7 +74,7 @@
<plugin> <plugin>
<groupId>io.fabric8</groupId> <groupId>io.fabric8</groupId>
<artifactId>docker-maven-plugin</artifactId> <artifactId>docker-maven-plugin</artifactId>
<version>0.20.1</version> <version>0.21.0</version>
<configuration> <configuration>
<logStdout>true</logStdout> <logStdout>true</logStdout>
@ -87,7 +87,7 @@
<name>acme4j/pebble:${project.version}</name> <name>acme4j/pebble:${project.version}</name>
<build> <build>
<from>golang:1.7</from> <from>golang:latest</from>
<optimise>true</optimise> <optimise>true</optimise>
<runCmds> <runCmds>
<run>go get -u -v -d gopkg.in/square/go-jose.v2</run> <run>go get -u -v -d gopkg.in/square/go-jose.v2</run>
@ -100,7 +100,10 @@
<port>14000</port> <port>14000</port>
</ports> </ports>
<cmd> <cmd>
<shell>pebble -config /etc/pebble/pebble-config.json</shell> <shell>
echo "nameserver $(grep 'bammbamm' /etc/hosts|cut -f1)">/etc/resolv.conf; \
pebble -config /etc/pebble/pebble-config.json
</shell>
</cmd> </cmd>
<assembly> <assembly>
<mode>dir</mode> <mode>dir</mode>
@ -115,12 +118,58 @@
</build> </build>
<run> <run>
<network> <ports>
<mode>host</mode> <port>14000:14000</port>
</network> </ports>
<extraHosts> <links>
<host>example.com:127.0.0.1</host> <link>bammbamm</link>
</extraHosts> </links>
<wait>
<log>Pebble running</log>
</wait>
<!-- DNS must point to bammbamm, but we need the IP here...
See the "echo" line in the build/cmd above.
<dns>
<host>bammbamm</host>
</dns>
-->
<!-- See https://github.com/letsencrypt/pebble/issues/36
<env>
<PEBBLE_VA_NOSLEEP>1</PEBBLE_VA_NOSLEEP>
</env>
-->
</run>
</image>
<image>
<alias>bammbamm</alias>
<name>acme4j/bammbamm:${project.version}</name>
<build>
<from>openjdk:8-jre</from>
<ports>
<port>53/udp</port>
<port>5001</port>
<port>5002</port>
<port>14001</port>
</ports>
<cmd>
<shell>
java -cp "/maven/*" org.shredzone.acme4j.it.BammBamm
</shell>
</cmd>
<assembly>
<descriptorRef>artifact-with-dependencies</descriptorRef>
</assembly>
</build>
<run>
<hostname>bammbamm</hostname>
<ports>
<port>14001:14001</port>
</ports>
<wait>
<log>Bammbamm running</log>
</wait>
</run> </run>
</image> </image>
</images> </images>
@ -159,11 +208,21 @@
<artifactId>nanohttpd</artifactId> <artifactId>nanohttpd</artifactId>
<version>${nanohttpd.version}</version> <version>${nanohttpd.version}</version>
</dependency> </dependency>
<dependency>
<groupId>org.nanohttpd</groupId>
<artifactId>nanohttpd-nanolets</artifactId>
<version>${nanohttpd.version}</version>
</dependency>
<dependency> <dependency>
<groupId>dnsjava</groupId> <groupId>dnsjava</groupId>
<artifactId>dnsjava</artifactId> <artifactId>dnsjava</artifactId>
<version>${dnsjava.version}</version> <version>${dnsjava.version}</version>
</dependency> </dependency>
<dependency>
<groupId>org.apache.httpcomponents</groupId>
<artifactId>httpclient</artifactId>
<version>${httpclient.version}</version>
</dependency>
<dependency> <dependency>
<groupId>org.slf4j</groupId> <groupId>org.slf4j</groupId>

View File

@ -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<String, String> urlParams, IHTTPSession session) throws Exception;
@Override
public Response post(UriResource uriResource, Map<String, String> 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<String, String> 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<String, String> urlParams, IHTTPSession session) {
return get(uriResource, urlParams, session);
}
@Override
public Response delete(UriResource uriResource, Map<String, String> urlParams, IHTTPSession session) {
return get(uriResource, urlParams, session);
}
@Override
public Response other(String method, UriResource uriResource, Map<String, String> urlParams, IHTTPSession session) {
return get(uriResource, urlParams, session);
}
}

View File

@ -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.
* <p>
* <em>WARNING:</em> 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();
}
}

View File

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

View File

@ -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<String, String> 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<String, String> 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<String, String> 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<String, String> urlParams, IHTTPSession session) throws Exception {
String domain = urlParams.get("domain");
DnsServer server = BammBamm.instance().getDnsServer();
server.removeTxtRecord(domain);
}
}
}

View File

@ -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<String, String> 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<String, String> urlParams, IHTTPSession session) throws Exception {
String token = urlParams.get("token");
HttpServer server = BammBamm.instance().getHttpServer();
server.removeToken(token);
}
}
}

View File

@ -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<String, String> 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<String, String> urlParams, IHTTPSession session) throws Exception {
String alias = urlParams.get("alias");
TlsSniServer server = BammBamm.instance().getTlsSniServer();
server.removeCertificate(alias);
}
}
}

View File

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

View File

@ -23,15 +23,12 @@ import java.security.cert.X509Certificate;
import java.time.Duration; import java.time.Duration;
import java.time.Instant; import java.time.Instant;
import org.junit.AfterClass;
import org.junit.BeforeClass;
import org.junit.Ignore;
import org.junit.Test; import org.junit.Test;
import org.shredzone.acme4j.Account;
import org.shredzone.acme4j.AccountBuilder;
import org.shredzone.acme4j.Authorization; import org.shredzone.acme4j.Authorization;
import org.shredzone.acme4j.Certificate; import org.shredzone.acme4j.Certificate;
import org.shredzone.acme4j.Order; import org.shredzone.acme4j.Order;
import org.shredzone.acme4j.Account;
import org.shredzone.acme4j.AccountBuilder;
import org.shredzone.acme4j.Session; import org.shredzone.acme4j.Session;
import org.shredzone.acme4j.Status; import org.shredzone.acme4j.Status;
import org.shredzone.acme4j.challenge.Challenge; 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.challenge.TlsSni02Challenge;
import org.shredzone.acme4j.exception.AcmeException; import org.shredzone.acme4j.exception.AcmeException;
import org.shredzone.acme4j.exception.AcmeLazyLoadingException; 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.CSRBuilder;
import org.shredzone.acme4j.util.CertificateUtils; import org.shredzone.acme4j.util.CertificateUtils;
@ -51,38 +45,12 @@ import org.shredzone.acme4j.util.CertificateUtils;
*/ */
public class OrderIT extends PebbleITBase { 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 final String TEST_DOMAIN = "example.com";
private static TlsSniServer tlsSniServer; private final String bammbammUrl = System.getProperty("bammbammUrl", "http://localhost:14001");
private static HttpServer httpServer; private final String bammbammHostname = System.getProperty("bammbammHostname", "bammbamm");
private static DnsServer dnsServer;
@BeforeClass private BammBammClient client = new BammBammClient(bammbammUrl);
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();
}
/** /**
* Test if a certificate can be ordered via tns-sni-02 challenge. * 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( X509Certificate cert = CertificateUtils.createTlsSni02Certificate(
challengeKey, challenge.getSubject(), challenge.getSanB()); 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; return challenge;
}); });
} }
@ -112,7 +85,12 @@ public class OrderIT extends PebbleITBase {
Http01Challenge challenge = auth.findChallenge(Http01Challenge.TYPE); Http01Challenge challenge = auth.findChallenge(Http01Challenge.TYPE);
assertThat(challenge, is(notNullValue())); 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; return challenge;
}); });
} }
@ -121,13 +99,19 @@ public class OrderIT extends PebbleITBase {
* Test if a certificate can be ordered via dns-01 challenge. * Test if a certificate can be ordered via dns-01 challenge.
*/ */
@Test @Test
@Ignore // TODO PEBBLE: cannot query our dnsServer yet...
public void testDnsValidation() throws Exception { public void testDnsValidation() throws Exception {
orderCertificate(TEST_DOMAIN, auth -> { orderCertificate(TEST_DOMAIN, auth -> {
Dns01Challenge challenge = auth.findChallenge(Dns01Challenge.TYPE); Dns01Challenge challenge = auth.findChallenge(Dns01Challenge.TYPE);
assertThat(challenge, is(notNullValue())); 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; return challenge;
}); });
} }
@ -173,7 +157,7 @@ public class OrderIT extends PebbleITBase {
challenge.trigger(); challenge.trigger();
await() await()
.pollInterval(3, SECONDS) .pollInterval(1, SECONDS)
.timeout(30, SECONDS) .timeout(30, SECONDS)
.conditionEvaluationListener(cond -> updateAuth(auth)) .conditionEvaluationListener(cond -> updateAuth(auth))
.until(auth::getStatus, not(Status.PENDING)); .until(auth::getStatus, not(Status.PENDING));

View File

@ -19,7 +19,10 @@ import static org.junit.Assert.assertThat;
import java.net.URI; import java.net.URI;
import java.net.URL; import java.net.URL;
import java.security.KeyPair; import java.security.KeyPair;
import java.util.ArrayList;
import java.util.List;
import org.junit.After;
import org.shredzone.acme4j.util.KeyPairUtils; import org.shredzone.acme4j.util.KeyPairUtils;
/** /**
@ -31,10 +34,23 @@ import org.shredzone.acme4j.util.KeyPairUtils;
* {@code pebbleHost} and {@code pebblePort} respectively. * {@code pebbleHost} and {@code pebblePort} respectively.
*/ */
public abstract class PebbleITBase { public abstract class PebbleITBase {
private final String pebbleHost = System.getProperty("pebbleHost", "localhost"); private final String pebbleHost = System.getProperty("pebbleHost", "localhost");
private final int pebblePort = Integer.parseInt(System.getProperty("pebblePort", "14000")); private final int pebblePort = Integer.parseInt(System.getProperty("pebblePort", "14000"));
private final List<CleanupCallback> 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. * @return The {@link URI} of the pebble server to test against.
*/ */
@ -66,4 +82,9 @@ public abstract class PebbleITBase {
assertThat(url.getPath(), not(isEmptyOrNullString())); assertThat(url.getPath(), not(isEmptyOrNullString()));
} }
@FunctionalInterface
public static interface CleanupCallback {
void cleanup() throws Exception;
}
} }

View File

@ -53,6 +53,7 @@
<properties> <properties>
<bouncycastle.version>1.57</bouncycastle.version> <bouncycastle.version>1.57</bouncycastle.version>
<dnsjava.version>2.1.7</dnsjava.version> <dnsjava.version>2.1.7</dnsjava.version>
<httpclient.version>4.5.3</httpclient.version>
<jose4j.version>0.6.0</jose4j.version> <jose4j.version>0.6.0</jose4j.version>
<nanohttpd.version>2.3.1</nanohttpd.version> <nanohttpd.version>2.3.1</nanohttpd.version>
<slf4j.version>1.7.25</slf4j.version> <slf4j.version>1.7.25</slf4j.version>

View File

@ -39,8 +39,10 @@ Now just have a look at [this source code](https://github.com/shred/acme4j/blob/
Running Integration Tests 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.