mirror of https://github.com/shred/acme4j
Use official pebble docker image
parent
dc17433634
commit
c0c6d1a13a
|
@ -50,18 +50,6 @@ public class Http01Challenge extends TokenChallenge {
|
|||
return super.getToken();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the authorization string to be used for the response.
|
||||
* <p>
|
||||
* <em>NOTE:</em> The response file must only contain the returned String (UTF-8
|
||||
* or ASCII encoded). There must not be any other leading or trailing characters
|
||||
* (like white-spaces or line breaks). Otherwise the challenge will fail.
|
||||
*/
|
||||
@Override
|
||||
public String getAuthorization() {
|
||||
return super.getAuthorization();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected boolean acceptable(String type) {
|
||||
return TYPE.equals(type);
|
||||
|
|
|
@ -65,7 +65,7 @@ public class TokenChallenge extends Challenge {
|
|||
* The default is {@code token + '.' + base64url(jwkThumbprint)}. Subclasses may
|
||||
* override this method if a different algorithm is used.
|
||||
*/
|
||||
protected String getAuthorization() {
|
||||
public String getAuthorization() {
|
||||
try {
|
||||
PublicKey pk = getLogin().getKeyPair().getPublic();
|
||||
PublicJsonWebKey jwk = PublicJsonWebKey.Factory.newPublicJwk(pk);
|
||||
|
|
|
@ -29,10 +29,7 @@
|
|||
<description>acme4j Integration Tests</description>
|
||||
|
||||
<properties>
|
||||
<!-- alpn-boot version must match the java version used in bammbamm -->
|
||||
<!-- see: https://www.eclipse.org/jetty/documentation/9.4.x/alpn-chapter.html#alpn-versions -->
|
||||
<alpn-java.version>8u181</alpn-java.version>
|
||||
<alpn-boot.version>8.1.12.v20180117</alpn-boot.version>
|
||||
<pebble.version>latest</pebble.version>
|
||||
|
||||
<skipITs>true</skipITs>
|
||||
<sonar.coverage.exclusions>src/main/java/org/shredzone/acme4j/**</sonar.coverage.exclusions>
|
||||
|
@ -119,38 +116,7 @@
|
|||
<images>
|
||||
<image>
|
||||
<alias>pebble</alias>
|
||||
<name>acme4j/pebble:${project.version}</name>
|
||||
|
||||
<build>
|
||||
<from>golang:1.11</from>
|
||||
<optimise>true</optimise>
|
||||
<runCmds>
|
||||
<run>go get -u -v -d gopkg.in/square/go-jose.v2</run>
|
||||
<run>go get -u -v -d github.com/jmhodges/clock</run>
|
||||
<run>go get -u -v -d github.com/letsencrypt/pebble || true</run>
|
||||
<run>go test github.com/letsencrypt/pebble/...</run>
|
||||
<run>go install github.com/letsencrypt/pebble/...</run>
|
||||
</runCmds>
|
||||
<ports>
|
||||
<port>14000</port>
|
||||
</ports>
|
||||
<cmd>
|
||||
<shell>
|
||||
pebble -strict -dnsserver bammbamm:53 -config /etc/pebble/pebble-config.json
|
||||
</shell>
|
||||
</cmd>
|
||||
<assembly>
|
||||
<mode>dir</mode>
|
||||
<targetDir>/etc/pebble</targetDir>
|
||||
<inline>
|
||||
<fileSet>
|
||||
<directory>src/test/pebble</directory>
|
||||
<outputDirectory>.</outputDirectory>
|
||||
</fileSet>
|
||||
</inline>
|
||||
</assembly>
|
||||
</build>
|
||||
|
||||
<name>letsencrypt/pebble:${pebble.version}</name>
|
||||
<run>
|
||||
<namingStrategy>alias</namingStrategy>
|
||||
<ports>
|
||||
|
@ -159,6 +125,13 @@
|
|||
<links>
|
||||
<link>bammbamm</link>
|
||||
</links>
|
||||
<cmd>
|
||||
<exec>
|
||||
<arg>sh</arg>
|
||||
<arg>-c</arg>
|
||||
<arg>pebble -strict -dnsserver $(getent hosts bammbamm|cut -d' ' -f1):8053 -config /test/config/pebble-config.json</arg>
|
||||
</exec>
|
||||
</cmd>
|
||||
<wait>
|
||||
<log>Listening</log>
|
||||
</wait>
|
||||
|
@ -170,34 +143,22 @@
|
|||
</image>
|
||||
<image>
|
||||
<alias>bammbamm</alias>
|
||||
<name>acme4j/bammbamm:${project.version}</name>
|
||||
|
||||
<build>
|
||||
<from>openjdk:${alpn-java.version}-jre-alpine</from>
|
||||
<ports>
|
||||
<port>53/udp</port>
|
||||
<port>5001</port>
|
||||
<port>5002</port>
|
||||
<port>14001</port>
|
||||
</ports>
|
||||
<cmd>
|
||||
<shell>
|
||||
java -Xbootclasspath/p:/maven/alpn-boot-${alpn-boot.version}.jar -cp "/maven/*" org.shredzone.acme4j.it.BammBamm
|
||||
</shell>
|
||||
</cmd>
|
||||
<assembly>
|
||||
<descriptorRef>artifact-with-dependencies</descriptorRef>
|
||||
</assembly>
|
||||
</build>
|
||||
|
||||
<name>letsencrypt/pebble-challtestsrv:${pebble.version}</name>
|
||||
<run>
|
||||
<namingStrategy>alias</namingStrategy>
|
||||
<hostname>bammbamm</hostname>
|
||||
<ports>
|
||||
<port>14001:14001</port>
|
||||
<port>8055:8055</port>
|
||||
</ports>
|
||||
<cmd>
|
||||
<exec>
|
||||
<arg>sh</arg>
|
||||
<arg>-c</arg>
|
||||
<arg>pebble-challtestsrv -defaultIPv6 "" -defaultIPv4 $(getent hosts bammbamm|cut -d' ' -f1)</arg>
|
||||
</exec>
|
||||
</cmd>
|
||||
<wait>
|
||||
<log>Bammbamm running</log>
|
||||
<log>Starting management server</log>
|
||||
</wait>
|
||||
</run>
|
||||
</image>
|
||||
|
@ -219,26 +180,6 @@
|
|||
<version>${project.version}</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>org.mortbay.jetty.alpn</groupId>
|
||||
<artifactId>alpn-boot</artifactId>
|
||||
<version>${alpn-boot.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.nanohttpd</groupId>
|
||||
<artifactId>nanohttpd</artifactId>
|
||||
<version>${nanohttpd.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.nanohttpd</groupId>
|
||||
<artifactId>nanohttpd-nanolets</artifactId>
|
||||
<version>${nanohttpd.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>dnsjava</groupId>
|
||||
<artifactId>dnsjava</artifactId>
|
||||
<version>${dnsjava.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.apache.httpcomponents</groupId>
|
||||
<artifactId>httpclient</artifactId>
|
||||
|
|
|
@ -1,82 +0,0 @@
|
|||
/*
|
||||
* 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 javax.annotation.ParametersAreNonnullByDefault;
|
||||
|
||||
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.
|
||||
*/
|
||||
@ParametersAreNonnullByDefault
|
||||
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; //NOSONAR: the request fails on any 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);
|
||||
}
|
||||
|
||||
}
|
|
@ -1,156 +0,0 @@
|
|||
/*
|
||||
* 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 javax.annotation.ParametersAreNonnullByDefault;
|
||||
|
||||
import org.shredzone.acme4j.it.server.DnsServer;
|
||||
import org.shredzone.acme4j.it.server.HttpServer;
|
||||
import org.shredzone.acme4j.it.server.TlsAlpnServer;
|
||||
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-ALPN 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!
|
||||
*/
|
||||
@ParametersAreNonnullByDefault
|
||||
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 tlsAlpnPort;
|
||||
private final AppServer appServer;
|
||||
private final DnsServer dnsServer;
|
||||
private final HttpServer httpServer;
|
||||
private final TlsAlpnServer tlsAlpnServer;
|
||||
|
||||
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"));
|
||||
tlsAlpnPort = Integer.parseInt(bundle.getString("tlsAlpn.port"));
|
||||
|
||||
dnsServer = new DnsServer();
|
||||
httpServer = new HttpServer();
|
||||
tlsAlpnServer = new TlsAlpnServer();
|
||||
appServer = new AppServer(appPort);
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves the singleton instance of {@link BammBamm}.
|
||||
*
|
||||
* @return {@link BammBamm} singleton instance
|
||||
*/
|
||||
public static BammBamm instance() {
|
||||
return INSTANCE;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the {@link DnsServer} instance.
|
||||
*/
|
||||
public DnsServer getDnsServer() {
|
||||
return dnsServer;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the {@link HttpServer} instance.
|
||||
*/
|
||||
public HttpServer getHttpServer() {
|
||||
return httpServer;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the {@link TlsAlpnServer} instance.
|
||||
*/
|
||||
public TlsAlpnServer getTlsAlpnServer() {
|
||||
return tlsAlpnServer;
|
||||
}
|
||||
|
||||
/**
|
||||
* Starts the servers.
|
||||
*/
|
||||
public void start() {
|
||||
dnsServer.start(dnsPort);
|
||||
httpServer.start(httpPort);
|
||||
tlsAlpnServer.start(tlsAlpnPort);
|
||||
|
||||
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();
|
||||
tlsAlpnServer.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(TlsAlpnHandler.ADD, TlsAlpnHandler.Add.class);
|
||||
addRoute(TlsAlpnHandler.REMOVE, TlsAlpnHandler.Remove.class);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Start bammbamm. It runs until the Java process is stopped.
|
||||
*
|
||||
* @param args
|
||||
* Command line arguments
|
||||
*/
|
||||
public static void main(String[] args) {
|
||||
BammBamm.instance().start();
|
||||
}
|
||||
|
||||
}
|
|
@ -14,45 +14,39 @@
|
|||
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 java.util.Arrays;
|
||||
import java.util.Objects;
|
||||
|
||||
import javax.annotation.ParametersAreNonnullByDefault;
|
||||
|
||||
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.entity.ContentType;
|
||||
import org.apache.http.entity.StringEntity;
|
||||
import org.apache.http.impl.client.HttpClients;
|
||||
import org.apache.http.message.BasicNameValuePair;
|
||||
import org.apache.http.util.EntityUtils;
|
||||
import org.shredzone.acme4j.toolbox.JSONBuilder;
|
||||
|
||||
/**
|
||||
* A BammBamm client.
|
||||
* The BammBamm client connects to the pebble-challtestsrv.
|
||||
*/
|
||||
@ParametersAreNonnullByDefault
|
||||
public class BammBammClient {
|
||||
private static final HttpClient CLIENT = HttpClients.createDefault();
|
||||
|
||||
private final String baseUrl;
|
||||
|
||||
/**
|
||||
* Creates a new BammBamm client.
|
||||
*
|
||||
* @param baseUrl
|
||||
* Base URL of the BammBamm server to connect to.
|
||||
* Base URL of the pebble-challtestsrv server to connect to.
|
||||
*/
|
||||
public BammBammClient(String baseUrl) {
|
||||
this.baseUrl = baseUrl;
|
||||
this.baseUrl = Objects.requireNonNull(baseUrl) + '/';
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -64,10 +58,10 @@ public class BammBammClient {
|
|||
* Challenge to respond with
|
||||
*/
|
||||
public void httpAddToken(String token, String challenge) throws IOException {
|
||||
createRequest(HttpHandler.ADD)
|
||||
.arg(":token", token)
|
||||
.param("challenge", challenge)
|
||||
.submit();
|
||||
JSONBuilder jb = new JSONBuilder();
|
||||
jb.put("token", token);
|
||||
jb.put("content", challenge);
|
||||
sendRequest("add-http01", jb.toString());
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -77,9 +71,9 @@ public class BammBammClient {
|
|||
* Token to remove
|
||||
*/
|
||||
public void httpRemoveToken(String token) throws IOException {
|
||||
createRequest(HttpHandler.REMOVE)
|
||||
.arg(":token", token)
|
||||
.submit();
|
||||
JSONBuilder jb = new JSONBuilder();
|
||||
jb.put("token", token);
|
||||
sendRequest("del-http01", jb.toString());
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -93,10 +87,10 @@ public class BammBammClient {
|
|||
* 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();
|
||||
JSONBuilder jb = new JSONBuilder();
|
||||
jb.put("host", domain);
|
||||
jb.array("addresses", Arrays.asList(ip));
|
||||
sendRequest("add-a", jb.toString());
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -106,9 +100,9 @@ public class BammBammClient {
|
|||
* Domain to remove the A Record from
|
||||
*/
|
||||
public void dnsRemoveARecord(String domain) throws IOException {
|
||||
createRequest(DnsHandler.REMOVE_A_RECORD)
|
||||
.arg(":domain", domain)
|
||||
.submit();
|
||||
JSONBuilder jb = new JSONBuilder();
|
||||
jb.put("host", domain);
|
||||
sendRequest("clear-a", jb.toString());
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -121,10 +115,10 @@ public class BammBammClient {
|
|||
* 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();
|
||||
JSONBuilder jb = new JSONBuilder();
|
||||
jb.put("host", domain + '.');
|
||||
jb.put("value", txt);
|
||||
sendRequest("set-txt", jb.toString());
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -134,147 +128,60 @@ public class BammBammClient {
|
|||
* Domain to remove the TXT Record from
|
||||
*/
|
||||
public void dnsRemoveTxtRecord(String domain) throws IOException {
|
||||
createRequest(DnsHandler.REMOVE_TXT_RECORD)
|
||||
.arg(":domain", domain)
|
||||
.submit();
|
||||
JSONBuilder jb = new JSONBuilder();
|
||||
jb.put("host", domain + '.');
|
||||
sendRequest("clear-txt", jb.toString());
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a certificate for TLS-ALPN 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 name to respond to
|
||||
* @param domain
|
||||
* Certificate domain to be added
|
||||
* @param keyauth
|
||||
* Key authorization to be used for validation
|
||||
*/
|
||||
public void tlsAlpnAddCertificate(String alias, PrivateKey privateKey, X509Certificate cert) throws IOException {
|
||||
try {
|
||||
createRequest(TlsAlpnHandler.ADD)
|
||||
.arg(":alias", alias)
|
||||
.param("privateKey", privateKey.getEncoded())
|
||||
.param("cert", cert.getEncoded())
|
||||
.submit();
|
||||
} catch (CertificateEncodingException ex) {
|
||||
throw new IOException(ex);
|
||||
}
|
||||
public void tlsAlpnAddCertificate(String domain, String keyauth) throws IOException {
|
||||
JSONBuilder jb = new JSONBuilder();
|
||||
jb.put("host", domain);
|
||||
jb.put("content", keyauth);
|
||||
sendRequest("add-tlsalpn01", jb.toString());
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes a certificate.
|
||||
*
|
||||
* @param alias
|
||||
* Certificate alias to remove
|
||||
* @param domain
|
||||
* Certificate domain to be removed
|
||||
*/
|
||||
public void tlsAlpnRemoveCertificate(String alias) throws IOException {
|
||||
createRequest(TlsAlpnHandler.REMOVE)
|
||||
.arg(":alias", alias)
|
||||
.submit();
|
||||
public void tlsAlpnRemoveCertificate(String domain) throws IOException {
|
||||
JSONBuilder jb = new JSONBuilder();
|
||||
jb.put("host", domain);
|
||||
sendRequest("del-tlsalpn01", jb.toString());
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new {@link Request} object.
|
||||
* Sends a request to the pebble-challtestsrv.
|
||||
*
|
||||
* @param call
|
||||
* Path to be called
|
||||
* @return Created {@link Request} object
|
||||
* Endpoint to be called
|
||||
* @param body
|
||||
* JSON body
|
||||
*/
|
||||
private Request createRequest(String call) {
|
||||
return new Request(baseUrl, call);
|
||||
}
|
||||
private void sendRequest(String call, String body) throws IOException {
|
||||
try {
|
||||
HttpPost httppost = new HttpPost(baseUrl + call);
|
||||
httppost.setEntity(new StringEntity(body, ContentType.APPLICATION_JSON));
|
||||
|
||||
/**
|
||||
* This class helps to assemble and invoke a HTTP POST request.
|
||||
*/
|
||||
@ParametersAreNonnullByDefault
|
||||
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");
|
||||
HttpResponse response = CLIENT.execute(httppost);
|
||||
|
||||
private final List<NameValuePair> params = new ArrayList<>();
|
||||
private final String baseUrl;
|
||||
private String call;
|
||||
EntityUtils.consume(response.getEntity());
|
||||
|
||||
/**
|
||||
* 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);
|
||||
if (response.getStatusLine().getStatusCode() != HttpStatus.SC_OK) {
|
||||
throw new IOException(response.getStatusLine().getReasonPhrase());
|
||||
}
|
||||
} catch (ClientProtocolException ex) {
|
||||
throw new IOException(ex);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -1,98 +0,0 @@
|
|||
/*
|
||||
* 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 javax.annotation.ParametersAreNonnullByDefault;
|
||||
|
||||
import org.shredzone.acme4j.it.server.DnsServer;
|
||||
|
||||
import fi.iki.elonen.NanoHTTPD.IHTTPSession;
|
||||
|
||||
/**
|
||||
* Request handler for all {@code dns-01} related requests.
|
||||
*/
|
||||
@ParametersAreNonnullByDefault
|
||||
public final 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";
|
||||
|
||||
private DnsHandler() {
|
||||
// this class cannot be instanciated.
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds an A Record.
|
||||
*/
|
||||
@ParametersAreNonnullByDefault
|
||||
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));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes an A Record.
|
||||
*/
|
||||
@ParametersAreNonnullByDefault
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a TXT Record.
|
||||
*/
|
||||
@ParametersAreNonnullByDefault
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes a TXT Record.
|
||||
*/
|
||||
@ParametersAreNonnullByDefault
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
|
@ -1,66 +0,0 @@
|
|||
/*
|
||||
* 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 javax.annotation.ParametersAreNonnullByDefault;
|
||||
|
||||
import org.shredzone.acme4j.it.server.HttpServer;
|
||||
|
||||
import fi.iki.elonen.NanoHTTPD.IHTTPSession;
|
||||
|
||||
/**
|
||||
* Request handler for all {@code http-01} related requests.
|
||||
*/
|
||||
@ParametersAreNonnullByDefault
|
||||
public final class HttpHandler {
|
||||
|
||||
public static final String ADD = "/http/add/:token";
|
||||
public static final String REMOVE = "/http/remove/:token";
|
||||
|
||||
private HttpHandler() {
|
||||
// this class cannot be instanciated.
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a HTTP challenge.
|
||||
*/
|
||||
@ParametersAreNonnullByDefault
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes a HTTP challenge.
|
||||
*/
|
||||
@ParametersAreNonnullByDefault
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
|
@ -1,80 +0,0 @@
|
|||
/*
|
||||
* acme4j - Java ACME client
|
||||
*
|
||||
* Copyright (C) 2018 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.TlsAlpnServer;
|
||||
|
||||
import fi.iki.elonen.NanoHTTPD.IHTTPSession;
|
||||
|
||||
/**
|
||||
* Request handler for all {@code tls-alpn-01} related requests.
|
||||
*/
|
||||
public final class TlsAlpnHandler {
|
||||
|
||||
public static final String ADD = "/tlsalpn/add/:alias";
|
||||
public static final String REMOVE = "/tlsalpn/remove/:alias";
|
||||
|
||||
private TlsAlpnHandler() {
|
||||
// this class cannot be instanciated.
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds an TLS-ALPN certificate.
|
||||
*/
|
||||
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)));
|
||||
|
||||
TlsAlpnServer server = BammBamm.instance().getTlsAlpnServer();
|
||||
server.addCertificate(alias, privateKey, cert);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes an TLS-ALPN certificate.
|
||||
*/
|
||||
public static class Remove extends AbstractResponder {
|
||||
@Override
|
||||
public void handle(Map<String, String> urlParams, IHTTPSession session) throws Exception {
|
||||
String alias = urlParams.get("alias");
|
||||
|
||||
TlsAlpnServer server = BammBamm.instance().getTlsAlpnServer();
|
||||
server.removeCertificate(alias);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
|
@ -1,219 +0,0 @@
|
|||
/*
|
||||
* 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.server;
|
||||
|
||||
import static java.util.Collections.synchronizedMap;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.net.DatagramPacket;
|
||||
import java.net.DatagramSocket;
|
||||
import java.net.Inet4Address;
|
||||
import java.net.InetAddress;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
import javax.annotation.ParametersAreNonnullByDefault;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.xbill.DNS.ARecord;
|
||||
import org.xbill.DNS.DClass;
|
||||
import org.xbill.DNS.Flags;
|
||||
import org.xbill.DNS.Header;
|
||||
import org.xbill.DNS.Message;
|
||||
import org.xbill.DNS.Name;
|
||||
import org.xbill.DNS.Rcode;
|
||||
import org.xbill.DNS.Record;
|
||||
import org.xbill.DNS.Section;
|
||||
import org.xbill.DNS.TXTRecord;
|
||||
import org.xbill.DNS.Type;
|
||||
|
||||
/**
|
||||
* A very simple and very stupid DNS server. It just responds to TXT and A queries of the
|
||||
* given domains, and refuses to answer anything else.
|
||||
* <p>
|
||||
* This server can be used to validate {@code dns-01} challenges, and to direct other
|
||||
* challenges to the mock servers.
|
||||
*/
|
||||
@ParametersAreNonnullByDefault
|
||||
public class DnsServer {
|
||||
private static final Logger LOG = LoggerFactory.getLogger(DnsServer.class);
|
||||
private static final int UDP_SIZE = 512;
|
||||
private static final long TTL = 300L;
|
||||
|
||||
private final Map<String, String> txtRecords = synchronizedMap(new HashMap<>());
|
||||
private final Map<String, InetAddress> aRecords = synchronizedMap(new HashMap<>());
|
||||
private Thread thread = null;
|
||||
private volatile boolean running = false;
|
||||
private volatile boolean listening = false;
|
||||
|
||||
/**
|
||||
* Adds a TXT record to the DNS server. If the domain already has a TXT record
|
||||
* attached, it will be replaced.
|
||||
*
|
||||
* @param domain
|
||||
* Domain to attach the TXT record to
|
||||
* @param txt
|
||||
* TXT record to attach
|
||||
*/
|
||||
public void addTxtRecord(String domain, String txt) {
|
||||
txtRecords.put(domain.replaceAll("\\.$", ""), txt);
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes a TXT record from the domain.
|
||||
*
|
||||
* @param domain
|
||||
* Domain to remove the TXT record from
|
||||
*/
|
||||
public void removeTxtRecord(String domain) {
|
||||
txtRecords.remove(domain);
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds an A record to the DNS server. If the domain already has an A record attached,
|
||||
* it will be replaced.
|
||||
*
|
||||
* @param domain
|
||||
* Domain to attach the A record to
|
||||
* @param ip
|
||||
* Target IP address
|
||||
*/
|
||||
public void addARecord(String domain, InetAddress ip) {
|
||||
if (!(ip instanceof Inet4Address)) {
|
||||
throw new IllegalArgumentException("must be an IPv4 address");
|
||||
}
|
||||
aRecords.put(domain.replaceAll("\\.$", ""), ip);
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes an A record from the domain.
|
||||
*
|
||||
* @param domain
|
||||
* Domain to remove the A record from
|
||||
*/
|
||||
public void removeARecord(String domain) {
|
||||
aRecords.remove(domain);
|
||||
}
|
||||
|
||||
/**
|
||||
* Starts the DNS server.
|
||||
*
|
||||
* @param port
|
||||
* Port to listen to
|
||||
*/
|
||||
public void start(int port) {
|
||||
if (thread != null) {
|
||||
throw new IllegalStateException("Server is already running");
|
||||
}
|
||||
|
||||
running = true;
|
||||
thread = new Thread(() -> serveUDP(port));
|
||||
thread.setName("DNS server");
|
||||
thread.start();
|
||||
LOG.info("dns-01 server listening at port {}", port);
|
||||
}
|
||||
|
||||
/**
|
||||
* Stops the DNS server.
|
||||
*/
|
||||
public void stop() {
|
||||
if (thread != null) {
|
||||
running = false;
|
||||
thread.interrupt();
|
||||
thread = null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the server was started up and is listening to connections.
|
||||
*/
|
||||
public boolean isListening() {
|
||||
return listening;
|
||||
}
|
||||
|
||||
/**
|
||||
* Opens an UDP socket and processes incoming messages.
|
||||
*
|
||||
* @param port
|
||||
* Port to listen at
|
||||
*/
|
||||
private void serveUDP(int port) {
|
||||
try (DatagramSocket sock = new DatagramSocket(port)) {
|
||||
listening = true;
|
||||
while (running) {
|
||||
process(sock);
|
||||
}
|
||||
listening = false;
|
||||
} catch (IOException ex) {
|
||||
LOG.error("Failed to open UDP socket", ex);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Processes a DNS query.
|
||||
*
|
||||
* @param sock
|
||||
* Socket to listen to
|
||||
*/
|
||||
private void process(DatagramSocket sock) {
|
||||
try {
|
||||
byte[] in = new byte[UDP_SIZE];
|
||||
|
||||
// Read the question
|
||||
DatagramPacket indp = new DatagramPacket(in, UDP_SIZE);
|
||||
indp.setLength(UDP_SIZE);
|
||||
sock.receive(indp);
|
||||
Message msg = new Message(in);
|
||||
Header header = msg.getHeader();
|
||||
|
||||
Record question = msg.getQuestion();
|
||||
|
||||
// Prepare a response
|
||||
Message response = new Message(header.getID());
|
||||
response.getHeader().setFlag(Flags.QR);
|
||||
response.addRecord(question, Section.QUESTION);
|
||||
|
||||
Name name = question.getName();
|
||||
boolean hasRecords = false;
|
||||
|
||||
String txt = txtRecords.get(name.toString(true));
|
||||
if (question.getType() == Type.TXT && txt != null) {
|
||||
response.addRecord(new TXTRecord(name, DClass.IN, TTL, txt), Section.ANSWER);
|
||||
hasRecords = true;
|
||||
LOG.info("dns-01: {} {} IN TXT \"{}\"", name, TTL, txt);
|
||||
}
|
||||
|
||||
InetAddress a = aRecords.get(name.toString(true));
|
||||
if (question.getType() == Type.A && a != null) {
|
||||
response.addRecord(new ARecord(name, DClass.IN, TTL, a), Section.ANSWER);
|
||||
hasRecords = true;
|
||||
LOG.info("dns-01: {} {} IN A {}", name, TTL, a.getHostAddress());
|
||||
}
|
||||
|
||||
if (!hasRecords) {
|
||||
response.getHeader().setRcode(Rcode.NXDOMAIN);
|
||||
LOG.warn("dns-01: Cannot answer: {}", question);
|
||||
}
|
||||
|
||||
// Send the response
|
||||
byte[] resp = response.toWire();
|
||||
DatagramPacket outdp = new DatagramPacket(resp, resp.length, indp.getAddress(), indp.getPort());
|
||||
sock.send(outdp);
|
||||
} catch (Exception ex) {
|
||||
LOG.error("Failed to process query", ex);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
|
@ -1,134 +0,0 @@
|
|||
/*
|
||||
* 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.server;
|
||||
|
||||
import static java.util.Collections.synchronizedMap;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.UncheckedIOException;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
import javax.annotation.ParametersAreNonnullByDefault;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import fi.iki.elonen.NanoHTTPD;
|
||||
import fi.iki.elonen.NanoHTTPD.Response.Status;
|
||||
|
||||
/**
|
||||
* A very simple web server that will answer at the {@code .well-known/acme-challenge}
|
||||
* path, returning the challenge to the given token.
|
||||
* <p>
|
||||
* This server can be used to validate {@code http-01} challenges.
|
||||
*/
|
||||
@ParametersAreNonnullByDefault
|
||||
public class HttpServer {
|
||||
private static final Logger LOG = LoggerFactory.getLogger(HttpServer.class);
|
||||
|
||||
private static final String TOKEN_PATH = "/.well-known/acme-challenge/";
|
||||
private static final Pattern TOKEN_PATTERN = Pattern.compile("^" + Pattern.quote(TOKEN_PATH) + "([^/]+)$");
|
||||
private static final String TYPE_TEXT_PLAIN = "text/plain";
|
||||
|
||||
private final Map<String, String> tokenMap = synchronizedMap(new HashMap<>());
|
||||
private NanoHTTPD server;
|
||||
|
||||
/**
|
||||
* Adds a token to the server's well-known challenges. If the token was already set,
|
||||
* the challenge will be replaced.
|
||||
*
|
||||
* @param token
|
||||
* Token the server will respond to
|
||||
* @param challenge
|
||||
* Challenge the server will respond with
|
||||
*/
|
||||
public void addToken(String token, String challenge) {
|
||||
tokenMap.put(token, challenge);
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes a token from the server's well-known challenges.
|
||||
*
|
||||
* @param token
|
||||
* Token to remove
|
||||
*/
|
||||
public void removeToken(String token) {
|
||||
tokenMap.remove(token);
|
||||
}
|
||||
|
||||
/**
|
||||
* Starts the HTTP server.
|
||||
*
|
||||
* @param port
|
||||
* Port to listen at
|
||||
*/
|
||||
public void start(int port) {
|
||||
if (server != null) {
|
||||
throw new IllegalStateException("Server is already running");
|
||||
}
|
||||
|
||||
server = new NanoHTTPD(port) {
|
||||
@Override
|
||||
public Response serve(IHTTPSession session) {
|
||||
String path = session.getUri().replaceAll("//+", "/");
|
||||
|
||||
Matcher m = TOKEN_PATTERN.matcher(path);
|
||||
if (!m.matches()) {
|
||||
return newFixedLengthResponse(Status.NOT_FOUND, TYPE_TEXT_PLAIN, "not found: "+ path + "\n");
|
||||
}
|
||||
|
||||
String token = m.group(1);
|
||||
String content = tokenMap.get(token);
|
||||
|
||||
if (content == null) {
|
||||
LOG.warn("http-01: unknown token " + token);
|
||||
return newFixedLengthResponse(Status.NOT_FOUND, TYPE_TEXT_PLAIN, "unknown token: "+ token + "\n");
|
||||
}
|
||||
|
||||
LOG.info("http-01: " + token + " -> " + content);
|
||||
return newFixedLengthResponse(Status.OK, TYPE_TEXT_PLAIN, content);
|
||||
}
|
||||
};
|
||||
|
||||
try {
|
||||
server.start(NanoHTTPD.SOCKET_READ_TIMEOUT, false);
|
||||
LOG.info("http-01 server listening at port {}", port);
|
||||
} catch (IOException ex) {
|
||||
LOG.error("Failed to start http-01 server", ex);
|
||||
server = null;
|
||||
throw new UncheckedIOException(ex);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Stops the HTTP server.
|
||||
*/
|
||||
public void stop() {
|
||||
if (server != null) {
|
||||
server.stop();
|
||||
server = null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the server was started up and is listening to connections.
|
||||
*/
|
||||
public boolean isListening() {
|
||||
return server != null && server.wasStarted();
|
||||
}
|
||||
|
||||
}
|
|
@ -1,262 +0,0 @@
|
|||
/*
|
||||
* acme4j - Java ACME client
|
||||
*
|
||||
* Copyright (C) 2018 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.server;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.security.KeyManagementException;
|
||||
import java.security.KeyStore;
|
||||
import java.security.KeyStoreException;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
import java.security.PrivateKey;
|
||||
import java.security.UnrecoverableKeyException;
|
||||
import java.security.cert.Certificate;
|
||||
import java.security.cert.CertificateException;
|
||||
import java.security.cert.CertificateParsingException;
|
||||
import java.security.cert.X509Certificate;
|
||||
import java.util.List;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import javax.net.ssl.KeyManager;
|
||||
import javax.net.ssl.KeyManagerFactory;
|
||||
import javax.net.ssl.SSLContext;
|
||||
import javax.net.ssl.SSLServerSocket;
|
||||
import javax.net.ssl.SSLServerSocketFactory;
|
||||
import javax.net.ssl.SSLSession;
|
||||
import javax.net.ssl.SSLSocket;
|
||||
import javax.net.ssl.TrustManager;
|
||||
import javax.net.ssl.TrustManagerFactory;
|
||||
|
||||
import org.bouncycastle.asn1.x509.GeneralName;
|
||||
import org.eclipse.jetty.alpn.ALPN;
|
||||
import org.shredzone.acme4j.challenge.TlsAlpn01Challenge;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
/**
|
||||
* A very simple TLS-ALPN server. It waits for a connection and performs a TLS handshake
|
||||
* returning the matching certificate to the requested domain.
|
||||
* <p>
|
||||
* This server can be used to validate {@code tls-alpn-01} challenges.
|
||||
*/
|
||||
public class TlsAlpnServer {
|
||||
private static final Logger LOG = LoggerFactory.getLogger(TlsAlpnServer.class);
|
||||
private static final char[] PASSWORD = "shibboleet".toCharArray();
|
||||
|
||||
private KeyStore keyStore = null;
|
||||
private Thread thread = null;
|
||||
private volatile boolean running = false;
|
||||
private volatile boolean listening = false;
|
||||
|
||||
/**
|
||||
* Adds a certificate to the set of known certificates.
|
||||
* <p>
|
||||
* The certificate's SAN is used for SNI.
|
||||
*
|
||||
* @param alias
|
||||
* Internal alias
|
||||
* @param privateKey
|
||||
* Private key to be used with this certificate
|
||||
* @param cert
|
||||
* {@link X509Certificate} to be added
|
||||
*/
|
||||
public void addCertificate(String alias, PrivateKey privateKey, X509Certificate cert) {
|
||||
initKeyStore();
|
||||
|
||||
try {
|
||||
keyStore.setKeyEntry(alias, privateKey, PASSWORD, new Certificate[] {cert});
|
||||
} catch (KeyStoreException ex) {
|
||||
throw new IllegalArgumentException("Failed to add certificate " + alias, ex);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes a certificate.
|
||||
*
|
||||
* @param alias
|
||||
* Internal alias of the certificate to remove
|
||||
*/
|
||||
public void removeCertificate(String alias) {
|
||||
initKeyStore();
|
||||
|
||||
try {
|
||||
keyStore.deleteEntry(alias);
|
||||
} catch (KeyStoreException ex) {
|
||||
throw new IllegalArgumentException("Failed to remove certificate " + alias, ex);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Starts the TlsAlpn server.
|
||||
*
|
||||
* @param port
|
||||
* Port to listen to
|
||||
*/
|
||||
public void start(int port) {
|
||||
if (thread != null) {
|
||||
throw new IllegalStateException("Server is already running");
|
||||
}
|
||||
|
||||
running = true;
|
||||
thread = new Thread(() -> serve(port));
|
||||
thread.setName("tls-alpn server");
|
||||
thread.start();
|
||||
LOG.info("tls-alpn server listening at port {}", port);
|
||||
}
|
||||
|
||||
/**
|
||||
* Stops the TlsAlpn server.
|
||||
*/
|
||||
public void stop() {
|
||||
if (thread != null) {
|
||||
running = false;
|
||||
thread.interrupt();
|
||||
thread = null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the server was started up and is listening to connections.
|
||||
*/
|
||||
public boolean isListening() {
|
||||
return listening;
|
||||
}
|
||||
|
||||
/**
|
||||
* Opens an SSL server socket and processes incoming requests.
|
||||
*
|
||||
* @param port
|
||||
* Port to listen at
|
||||
*/
|
||||
private void serve(int port) {
|
||||
SSLContext sslContext = createSSLContext();
|
||||
SSLServerSocketFactory sslServerSocketFactory = sslContext.getServerSocketFactory();
|
||||
|
||||
try (SSLServerSocket sslServerSocket = (SSLServerSocket)
|
||||
sslServerSocketFactory.createServerSocket(port)){
|
||||
listening = true;
|
||||
while (running) {
|
||||
process(sslServerSocket);
|
||||
}
|
||||
} catch (IOException ex) {
|
||||
LOG.error("Failed to create socket on port {}", port, ex);
|
||||
}
|
||||
|
||||
listening = false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Accept and process an incoming request. Only the TLS handshake is used here.
|
||||
* Incoming data is just consumed, and the socket is closed after that.
|
||||
*
|
||||
* @param sslServerSocket
|
||||
* {@link SSLServerSocket} to accept connections from
|
||||
*/
|
||||
private void process(SSLServerSocket sslServerSocket) {
|
||||
try (SSLSocket sslSocket = (SSLSocket) sslServerSocket.accept()) {
|
||||
ALPN.put(sslSocket, new ALPN.ServerProvider() {
|
||||
@Override
|
||||
public void unsupported() {
|
||||
ALPN.remove(sslSocket);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String select(List<String> protocols) {
|
||||
ALPN.remove(sslSocket);
|
||||
if (protocols.contains(TlsAlpn01Challenge.ACME_TLS_1_PROTOCOL)) {
|
||||
return TlsAlpn01Challenge.ACME_TLS_1_PROTOCOL;
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
sslSocket.setEnabledCipherSuites(sslSocket.getSupportedCipherSuites());
|
||||
sslSocket.startHandshake();
|
||||
|
||||
SSLSession sslSession = sslSocket.getSession();
|
||||
X509Certificate cert = (X509Certificate) sslSession.getLocalCertificates()[0];
|
||||
LOG.info("tls-alpn: {}", domainsToString(cert));
|
||||
|
||||
try (InputStream in = sslSocket.getInputStream()) {
|
||||
while (in.read() >= 0); //NOSONAR: intentional empty statement
|
||||
}
|
||||
} catch (Exception ex) {
|
||||
LOG.error("Failed to process request", ex);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Lazily initializes the {@link KeyStore} instance to be used. The key store is empty
|
||||
* after initialization.
|
||||
*/
|
||||
private void initKeyStore() {
|
||||
if (keyStore == null) {
|
||||
try {
|
||||
keyStore = KeyStore.getInstance("JKS");
|
||||
keyStore.load(null, null);
|
||||
} catch (KeyStoreException | NoSuchAlgorithmException | CertificateException
|
||||
| IOException ex) {
|
||||
throw new IllegalStateException("Failed to create key store", ex);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a {@link SSLContext} that uses the internal {@link KeyStore} for key and
|
||||
* trust management.
|
||||
*
|
||||
* @return {@link SSLContext} instance
|
||||
*/
|
||||
private SSLContext createSSLContext() {
|
||||
initKeyStore();
|
||||
|
||||
try {
|
||||
KeyManagerFactory keyManagerFactory = KeyManagerFactory.getInstance("NewSunX509");
|
||||
keyManagerFactory.init(keyStore, PASSWORD);
|
||||
KeyManager[] km = keyManagerFactory.getKeyManagers();
|
||||
|
||||
TrustManagerFactory trustManagerFactory = TrustManagerFactory.getInstance("SunX509");
|
||||
trustManagerFactory.init(keyStore);
|
||||
TrustManager[] tm = trustManagerFactory.getTrustManagers();
|
||||
|
||||
SSLContext sslContext = SSLContext.getInstance("TLSv1");
|
||||
sslContext.init(km, tm, null);
|
||||
|
||||
return sslContext;
|
||||
} catch (KeyManagementException | NoSuchAlgorithmException | KeyStoreException
|
||||
| UnrecoverableKeyException ex) {
|
||||
throw new IllegalStateException("Could not create SSLContext", ex);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Extracts all SANs of the given certificate and returns them as a string.
|
||||
*
|
||||
* @param cert
|
||||
* {@link X509Certificate} to read the SANs from
|
||||
* @return String of all SAN names joined together and separated by comma
|
||||
*/
|
||||
private String domainsToString(X509Certificate cert) {
|
||||
try {
|
||||
return cert.getSubjectAlternativeNames().stream()
|
||||
.filter(c -> ((Number) c.get(0)).intValue() == GeneralName.dNSName)
|
||||
.map(c -> (String) c.get(1))
|
||||
.collect(Collectors.joining(", "));
|
||||
} catch (CertificateParsingException ex) {
|
||||
throw new IllegalArgumentException("bad certificate", ex);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
|
@ -1,12 +0,0 @@
|
|||
|
||||
# Port where BammBamm is listening for new connections
|
||||
app.port = 14001
|
||||
|
||||
# DNS server port
|
||||
dns.port = 53
|
||||
|
||||
# HTTP server port
|
||||
http.port = 5002
|
||||
|
||||
# TLS-ALPN server port
|
||||
tlsAlpn.port = 5001
|
|
@ -43,7 +43,6 @@ import org.shredzone.acme4j.exception.AcmeException;
|
|||
import org.shredzone.acme4j.exception.AcmeServerException;
|
||||
import org.shredzone.acme4j.it.BammBammClient;
|
||||
import org.shredzone.acme4j.util.CSRBuilder;
|
||||
import org.shredzone.acme4j.util.CertificateUtils;
|
||||
|
||||
/**
|
||||
* Tests a complete certificate order with different challenges.
|
||||
|
@ -63,10 +62,8 @@ public class OrderIT extends PebbleITBase {
|
|||
Http01Challenge challenge = auth.findChallenge(Http01Challenge.TYPE);
|
||||
assertThat(challenge, is(notNullValue()));
|
||||
|
||||
client.dnsAddARecord(TEST_DOMAIN, getBammBammHostname());
|
||||
client.httpAddToken(challenge.getToken(), challenge.getAuthorization());
|
||||
|
||||
cleanup(() -> client.dnsRemoveARecord(TEST_DOMAIN));
|
||||
cleanup(() -> client.httpRemoveToken(challenge.getToken()));
|
||||
|
||||
return challenge;
|
||||
|
@ -105,20 +102,10 @@ public class OrderIT extends PebbleITBase {
|
|||
TlsAlpn01Challenge challenge = auth.findChallenge(TlsAlpn01Challenge.TYPE);
|
||||
assertThat(challenge, is(notNullValue()));
|
||||
|
||||
KeyPair challengeKey = createKeyPair();
|
||||
|
||||
X509Certificate cert = CertificateUtils.createTlsAlpn01Certificate(
|
||||
challengeKey,
|
||||
auth.getIdentifier().getDomain(),
|
||||
challenge.getAcmeValidation());
|
||||
|
||||
client.dnsAddARecord(TEST_DOMAIN, getBammBammHostname());
|
||||
client.tlsAlpnAddCertificate(
|
||||
auth.getIdentifier().getDomain(),
|
||||
challengeKey.getPrivate(),
|
||||
cert);
|
||||
challenge.getAuthorization());
|
||||
|
||||
cleanup(() -> client.dnsRemoveARecord(TEST_DOMAIN));
|
||||
cleanup(() -> client.tlsAlpnRemoveCertificate(auth.getIdentifier().getDomain()));
|
||||
|
||||
return challenge;
|
||||
|
@ -136,10 +123,8 @@ public class OrderIT extends PebbleITBase {
|
|||
Http01Challenge challenge = auth.findChallenge(Http01Challenge.TYPE);
|
||||
assertThat(challenge, is(notNullValue()));
|
||||
|
||||
client.dnsAddARecord(TEST_DOMAIN, getBammBammHostname());
|
||||
client.httpAddToken(challenge.getToken(), challenge.getAuthorization());
|
||||
|
||||
cleanup(() -> client.dnsRemoveARecord(TEST_DOMAIN));
|
||||
cleanup(() -> client.httpRemoveToken(challenge.getToken()));
|
||||
|
||||
return challenge;
|
||||
|
|
|
@ -37,13 +37,15 @@ import org.shredzone.acme4j.util.KeyPairUtils;
|
|||
* <a href="https://github.com/letsencrypt/pebble">Pebble</a> ACME test server at
|
||||
* localhost port 14000. The host and port can be changed via the system property
|
||||
* {@code pebbleHost} and {@code pebblePort} respectively.
|
||||
* <p>
|
||||
* Also, a running pebble-challtestsrv is required to listen on localhost port 8055. The
|
||||
* server's base URL can be changed via the system property {@code bammbammUrl}.
|
||||
*/
|
||||
public abstract class PebbleITBase {
|
||||
private final String pebbleHost = System.getProperty("pebbleHost", "localhost");
|
||||
private final int pebblePort = Integer.parseInt(System.getProperty("pebblePort", "14000"));
|
||||
|
||||
private final String bammbammUrl = System.getProperty("bammbammUrl", "http://localhost:14001");
|
||||
private final String bammbammHostname = System.getProperty("bammbammHostname", "bammbamm");
|
||||
private final String bammbammUrl = System.getProperty("bammbammUrl", "http://localhost:8055");
|
||||
|
||||
private BammBammClient bammBammClient;
|
||||
|
||||
|
@ -78,13 +80,6 @@ public abstract class PebbleITBase {
|
|||
return bammBammClient;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Hostname or IP address of the BammBamm server.
|
||||
*/
|
||||
protected String getBammBammHostname() {
|
||||
return bammbammHostname;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a fresh key pair.
|
||||
*
|
||||
|
|
|
@ -1,9 +0,0 @@
|
|||
{
|
||||
"pebble": {
|
||||
"listenAddress": "0.0.0.0:14000",
|
||||
"certificate": "/go/src/github.com/letsencrypt/pebble/test/certs/localhost/cert.pem",
|
||||
"privateKey": "/go/src/github.com/letsencrypt/pebble/test/certs/localhost/key.pem",
|
||||
"httpPort": 5002,
|
||||
"tlsPort": 5001
|
||||
}
|
||||
}
|
2
pom.xml
2
pom.xml
|
@ -52,10 +52,8 @@
|
|||
|
||||
<properties>
|
||||
<bouncycastle.version>1.60</bouncycastle.version>
|
||||
<dnsjava.version>2.1.8</dnsjava.version>
|
||||
<httpclient.version>4.5.6</httpclient.version>
|
||||
<jose4j.version>0.6.4</jose4j.version>
|
||||
<nanohttpd.version>2.3.1</nanohttpd.version>
|
||||
<slf4j.version>1.7.25</slf4j.version>
|
||||
<project.build.sourceEncoding>utf-8</project.build.sourceEncoding>
|
||||
<project.reporting.outputEncoding>utf-8</project.reporting.outputEncoding>
|
||||
|
|
Loading…
Reference in New Issue