+ * Pass the names of the domains as parameters.
+ */
+public class AcmeClient {
+ // File name of the User Key Pair
+ private static final File USER_KEY_FILE = new File("user.key");
+
+ // File name of the Domain Key Pair
+ private static final File DOMAIN_KEY_FILE = new File("domain.key");
+
+ // File name of the CSR
+ private static final File DOMAIN_CSR_FILE = new File("domain.csr");
+
+ // File name of the signed certificate
+ private static final File DOMAIN_CHAIN_FILE = new File("domain-chain.crt");
+
+ // RSA key size of generated key pairs
+ private static final int KEY_SIZE = 2048;
+
+ private static final Logger LOG = LoggerFactory.getLogger(AcmeClient.class);
+
+ /**
+ * Generates a certificate for the given domains. Also takes care for the registration process.
+ *
+ * @param domains
+ * Domains to get a common certificate for
+ */
+ public void fetchCertificate(Collection
+ * This is a simple way of finding your {@link Registration}. A better way is to get the URI of your new registration with {@link Registration#getLocation()} and store it somewhere. If you need to
+ * get access to your account later, reconnect to it via {@link Registration#bind(Session, URI)} by using the stored location.
+ *
+ * @param session
+ * {@link Session} to bind with
+ * @return {@link Registration} connected to your account
+ */
+ private Registration findOrRegisterAccount(Session session) throws AcmeException {
+ Registration reg;
+
+ try {
+ // Try to create a new Registration.
+ reg = new RegistrationBuilder().create(session);
+ LOG.info("Registered a new user, URI: " + reg.getLocation());
+
+ // This is a new account. Let the user accept the Terms of Service.
+ // We won't be able to authorize domains until the ToS is accepted.
+ URI agreement = reg.getAgreement();
+ LOG.info("Terms of Service: " + agreement);
+ acceptAgreement(reg, agreement);
+
+ } catch (AcmeConflictException ex) {
+ // The Key Pair is already registered. getLocation() contains the
+ // URL of the existing registration's location. Bind it to the session.
+ reg = Registration.bind(session, ex.getLocation());
+ LOG.info("Account does already exist, URI: " + reg.getLocation(), ex);
+ }
+
+ return reg;
+ }
+
+ /**
+ * Authorize a domain. It will be associated with your account, so you will be able to retrieve a signed certificate for the domain later.
+ *
+ * You need separate authorizations for subdomains (e.g. "www" subdomain). Wildcard certificates are not currently supported.
+ *
+ * @param reg
+ * {@link Registration} of your account
+ * @param domain
+ * Name of the domain to authorize
+ */
+ private void authorize(Registration reg, String domain) throws AcmeException {
+ // Authorize the domain.
+ Authorization auth = reg.authorizeDomain(domain);
+ LOG.info("Authorization for domain " + domain);
+
+ // Find the desired challenge and prepare it.
+ Challenge challenge = httpChallenge(auth, domain);
+
+ if (challenge == null) {
+ throw new AcmeException("No challenge found");
+ }
+
+ // If the challenge is already verified, there's no need to execute it again.
+ if (challenge.getStatus() == Status.VALID) {
+ return;
+ }
+
+ // Now trigger the challenge.
+ challenge.trigger();
+
+ // Poll for the challenge to complete.
+ try {
+ int attempts = 10;
+ while (challenge.getStatus() != Status.VALID && attempts-- > 0) {
+ // Did the authorization fail?
+ if (challenge.getStatus() == Status.INVALID) {
+ throw new AcmeException("Challenge failed... Giving up.");
+ }
+
+ // Wait for a few seconds
+ Thread.sleep(3000L);
+
+ // Then update the status
+ challenge.update();
+ }
+ } catch (InterruptedException ex) {
+ LOG.error("interrupted", ex);
+ Thread.currentThread().interrupt();
+ }
+
+ // All reattempts are used up and there is still no valid authorization?
+ if (challenge.getStatus() != Status.VALID) {
+ throw new AcmeException("Failed to pass the challenge for domain " + domain + ", ... Giving up.");
+ }
+ }
+
+ /**
+ * Prepares a HTTP challenge.
+ *
+ * The verification of this challenge expects a file with a certain content to be reachable at a given path under the domain to be tested.
+ *
+ * This example outputs instructions that need to be executed manually. In a production environment, you would rather generate this file automatically, or maybe use a servlet that returns
+ * {@link Http01Challenge#getAuthorization()}.
+ *
+ * @param auth
+ * {@link Authorization} to find the challenge in
+ * @param domain
+ * Domain name to be authorized
+ * @return {@link Challenge} to verify
+ */
+ public Challenge httpChallenge(Authorization auth, String domain) throws AcmeException {
+ // Find a single http-01 challenge
+ Http01Challenge challenge = auth.findChallenge(Http01Challenge.TYPE);
+ if (challenge == null) {
+ throw new AcmeException("Found no " + Http01Challenge.TYPE + " challenge, don't know what to do...");
+ }
+
+ // Output the challenge, wait for acknowledge...
+ LOG.info("Please create a file in your web server's base directory.");
+ LOG.info("It must be reachable at: http://" + domain + "/.well-known/acme-challenge/" + challenge.getToken());
+ LOG.info("File name: " + challenge.getToken());
+ LOG.info("Content: " + challenge.getAuthorization());
+ LOG.info("The file must not contain any leading or trailing whitespaces or line breaks!");
+ LOG.info("If you're ready, dismiss the dialog...");
+
+ StringBuilder message = new StringBuilder();
+ message.append("Please create a file in your web server's base directory.\n\n");
+ message.append("http://").append(domain).append("/.well-known/acme-challenge/").append(challenge.getToken())
+ .append("\n\n");
+ message.append("Content:\n\n");
+ message.append(challenge.getAuthorization());
+ acceptChallenge(message.toString());
+
+ return challenge;
+ }
+
+ /**
+ * Presents the instructions for preparing the challenge validation, and waits for dismissal. If the user cancelled the dialog, an exception is thrown.
+ *
+ * @param message
+ * Instructions to be shown in the dialog
+ */
+ public void acceptChallenge(String message) throws AcmeException {
+ int option = JOptionPane.showConfirmDialog(null, message, "Prepare Challenge", JOptionPane.OK_CANCEL_OPTION);
+ if (option == JOptionPane.CANCEL_OPTION) {
+ throw new AcmeException("User cancelled the challenge");
+ }
+ }
+
+ /**
+ * Presents the user a link to the Terms of Service, and asks for confirmation. If the user denies confirmation, an exception is thrown.
+ *
+ * @param reg
+ * {@link Registration} User's registration
+ * @param agreement
+ * {@link URI} of the Terms of Service
+ */
+ public void acceptAgreement(Registration reg, URI agreement) throws AcmeException {
+ int option = JOptionPane.showConfirmDialog(null, "Do you accept the Terms of Service?\n\n" + agreement,
+ "Accept ToS", JOptionPane.YES_NO_OPTION);
+ if (option == JOptionPane.NO_OPTION) {
+ throw new AcmeException("User did not accept Terms of Service");
+ }
+
+ // Motify the Registration and accept the agreement
+ reg.modify().setAgreement(agreement).commit();
+ LOG.info("Updated user's ToS");
+ }
+
+ /**
+ * Invokes this example.
+ *
+ * @param args
+ * Domains to get a certificate for
+ */
+ public static void main(String... args) {
+ if (args.length == 0) {
+ System.err.println("Usage: ClientTest