=== modified file 'build.gradle' --- build.gradle 2017-07-14 21:57:43 +0000 +++ build.gradle 2017-10-30 23:47:56 +0000 @@ -312,6 +312,8 @@ fwdClientServer group: 'org.apache.axis2', name: 'axis2-adb', version: '1.6.2' fwdClientServer group: 'javax.mail', name: 'mail', version: '1.4' fwdClientServer group: 'args4j', name: 'args4j', version: '2.33' + fwdClientServer group: 'org.shredzone.acme4j', name: 'acme4j-client', version: '0.10' + fwdClientServer group: 'org.shredzone.acme4j', name: 'acme4j-utils', version: '0.10' fwdServer group: 'org.postgresql', name: 'postgresql', version: '9.4.1211' fwdServer group: 'org.hibernate', name: 'hibernate-c3p0', version: '4.1.8.Final' === added file 'src/com/goldencode/p2j/security/AcmeClient.java' --- src/com/goldencode/p2j/security/AcmeClient.java 1970-01-01 00:00:00 +0000 +++ src/com/goldencode/p2j/security/AcmeClient.java 2017-10-31 12:51:57 +0000 @@ -0,0 +1,709 @@ +/* +** Module : AcmeClient.java +** Abstract : Implements ACME client to get certificates signed by well-known trusted authority. +** +** Copyright (c) 2017, Golden Code Development Corporation. +** +** -#- -I- --Date-- ---------------------------------Description---------------------------------- +** 001 SBI 20171030 First version. +*/ +/* +** This program is free software: you can redistribute it and/or modify +** it under the terms of the GNU Affero General Public License as +** published by the Free Software Foundation, either version 3 of the +** License, or (at your option) any later version. +** +** 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. See the +** GNU Affero General Public License for more details. +** +** You may find a copy of the GNU Affero GPL version 3 at the following +** location: https://www.gnu.org/licenses/agpl-3.0.en.html +** +** Additional terms under GNU Affero GPL version 3 section 7: +** +** Under Section 7 of the GNU Affero GPL version 3, the following additional +** terms apply to the works covered under the License. These additional terms +** are non-permissive additional terms allowed under Section 7 of the GNU +** Affero GPL version 3 and may not be removed by you. +** +** 0. Attribution Requirement. +** +** You must preserve all legal notices or author attributions in the covered +** work or Appropriate Legal Notices displayed by works containing the covered +** work. You may not remove from the covered work any author or developer +** credit already included within the covered work. +** +** 1. No License To Use Trademarks. +** +** This license does not grant any license or rights to use the trademarks +** Golden Code, FWD, any Golden Code or FWD logo, or any other trademarks +** of Golden Code Development Corporation. You are not authorized to use the +** name Golden Code, FWD, or the names of any author or contributor, for +** publicity purposes without written authorization. +** +** 2. No Misrepresentation of Affiliation. +** +** You may not represent yourself as Golden Code Development Corporation or FWD. +** +** You may not represent yourself for publicity purposes as associated with +** Golden Code Development Corporation, FWD, or any author or contributor to +** the covered work, without written authorization. +** +** 3. No Misrepresentation of Source or Origin. +** +** You may not represent the covered work as solely your work. All modified +** versions of the covered work must be marked in a reasonable way to make it +** clear that the modified work is not originating from Golden Code Development +** Corporation or FWD. All modified versions must contain the notices of +** attribution required in this license. +*/ + + +package com.goldencode.p2j.security; + + +import java.io.*; +import java.net.*; +import java.security.*; +import java.security.cert.*; +import java.util.*; +import java.util.concurrent.TimeUnit; + +import org.bouncycastle.jce.provider.*; +import org.kohsuke.args4j.*; +import org.shredzone.acme4j.*; +import org.shredzone.acme4j.Session; +import org.shredzone.acme4j.Certificate; +import org.shredzone.acme4j.challenge.*; +import org.shredzone.acme4j.exception.*; +import org.shredzone.acme4j.util.*; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import com.goldencode.p2j.main.ClientsToPortsGenerator; + + +/** +* Defines the ACME client to get trusted certificates for the target domains. +*/ +public class AcmeClient +{ + /** If the challenge is not accepted for this period, then the client process is stopped. */ + private static final long ACCEPT_CHALLENGE_TIMEOUT = TimeUnit.MINUTES.toMillis(1); + + /** The number of tries to get validated challenge */ + private static final int CHALLENGE_TRIES = 100; + + /** The polling interval between challenge updates */ + private static final long UPDATE_CHALLENGE_TIMEOUT = 3000; + + /** The user private certificate */ + private static final File USER_KEY_FILE = new File("user.key"); + + /** The user registration URI */ + private static final File USER_REG_FILE = new File("user.reg"); + + /** The target domain private certificate */ + private static final File DOMAIN_KEY_FILE = new File("domain.key"); + + /** The certificate request for the signed target domain certificate */ + private static final File DOMAIN_CSR_FILE = new File("domain.csr"); + + /** The signed certificate full chain */ + private static final File DOMAIN_CHAIN_FILE = new File("domain-chain.crt"); + + /** The signed domain certificate */ + private static final File DOMAIN_CRT_FILE = new File("domain.crt"); + + /** The default RSA key size of generated key pairs */ + private static final int KEY_SIZE = 2048; + + /** The class logger */ + private static final Logger LOG = LoggerFactory.getLogger(AcmeClient.class); + + /** "acme://letsencrypt.org/staging" */ + private final String acmeServerUri; + + /** The managed server host address, the domain IP address */ + private final String host; + + /** The managed server port */ + private final int port; + + /** The instance of the managed web server */ + private ManagedWebServer mws; + + /** The current sesion with ACME server */ + private Session session; + + /** The ACME client account */ + private Registration registration; + + /** + * Setups the ACME client to use the provided ACME server for its requests and the given + * ACME accessible host and port. + * + * @param acmeServerUri + * The ACME server URI + * @param host + * The ACME client host address + * @param port + * The ACME client port + */ + public AcmeClient(String acmeServerUri, String host, int port) + { + this.acmeServerUri = acmeServerUri; + this.host = host; + this.port = port; + } + + /** + * + * @param userKeyPair + * @param contacts + * @return + * @throws AcmeException + */ + public URI registerAccount(KeyPair userKeyPair, List contacts) + throws AcmeException + { + // Create a session for Let's Encrypt. + // Use "acme://letsencrypt.org" for production server + // https://acme-staging.api.letsencrypt.org/directory + session = new Session(acmeServerUri, userKeyPair); + + RegistrationBuilder builder = new RegistrationBuilder(); + + for(String contact : contacts) + { + builder.addContact(contact); + } + + try + { + registration = builder.create(session); + + URI agreement = registration.getAgreement(); + setAgreement(agreement); + } + catch (AcmeConflictException ex) + { + registration = Registration.bind(session, ex.getLocation()); + } + + return registration.getLocation(); + } + + /** + * + * @param userKeyPair + * @param regAccount + * @throws AcmeException + */ + public void loginAccount(KeyPair userKeyPair, URI regAccount) + throws AcmeException + { + session = new Session(acmeServerUri, userKeyPair); + + registration = Registration.bind(session, regAccount); + } + + /** + * + * @param contact + * @throws AcmeException + */ + public void addContact(String contact) + throws AcmeException + { + registration.modify().addContact(contact).commit(); + } + + /** + * Confirms the Terms of Service given by its URI. + * + * @param agreement + * The Terms of Service given by its URI + */ + public void setAgreement(URI agreement) + throws AcmeException + { + registration.modify().setAgreement(agreement).commit(); + } + + /** + * + * @param userKeyPair + * @throws AcmeException + */ + public void changeUserKey(KeyPair userKeyPair) + throws AcmeException + { + registration.changeKey(userKeyPair); + } + + /** + * Requests certificates for the given domains. + * + * @param domains + * Domain to get a common certificate for + */ + public void askCertificate(Map certRequestInfo, Collection domains) + throws IOException, AcmeException + { + if (session == null) + { + throw new IllegalStateException("The session has not been created yet."); + } + + // Separately authorize every requested domain. + for (String domain : domains) + { + authorize(domain); + try + { + mws.shutdown(); + } + catch (Exception e) + { + throw new AcmeException("Can't shutdown the managed server", e); + } + } + + // Load or create a key pair for the domains. + KeyPair domainKeyPair = loadOrCreateKeyPair(DOMAIN_KEY_FILE); + + // Generate a CSR for all of the domains, and sign it with the domain key pair. + CSRBuilder csrb = new CSRBuilder(); + + String country = certRequestInfo.get("C"); + + if (country != null) + { + csrb.setCountry(country); + } + + String loc = certRequestInfo.get("L"); + + if (loc != null) + { + csrb.setLocality(loc); + } + + String org = certRequestInfo.get("O"); + + if (org != null) + { + csrb.setOrganization(org); + } + + String orgUnit = certRequestInfo.get("OU"); + + if (orgUnit != null) + { + csrb.setOrganizationalUnit(orgUnit); + } + + csrb.addDomains(domains); + + csrb.sign(domainKeyPair); + + try (Writer out = new FileWriter(DOMAIN_CSR_FILE)) + { + csrb.write(out); + } + + Certificate certificate = registration.requestCertificate(csrb.getEncoded()); + + LOG.info("Success! The certificate for domains " + domains + " has been generated!"); + LOG.info("Certificate URI: " + certificate.getLocation()); + + // Download the leaf certificate and certificate chain. + X509Certificate cert = certificate.download(); + + // Write the leaf certificate + try (FileWriter fw = new FileWriter(DOMAIN_CRT_FILE)) + { + CertificateUtils.writeX509Certificate(cert, fw); + } + + X509Certificate[] chain = certificate.downloadChain(); + + // Write a combined file containing the certificate and chain. + try (FileWriter fw = new FileWriter(DOMAIN_CHAIN_FILE)) + { + CertificateUtils.writeX509CertificateChain(fw, cert, chain); + } + } + + /** + * Loads a key pair from specified file. If the file does not exist, + * a new key pair is generated and saved. + * + * @return {@link KeyPair}. + */ + private static KeyPair loadOrCreateKeyPair(File file) throws IOException + { + if (file.exists()) + { + try (FileReader fr = new FileReader(file)) + { + return KeyPairUtils.readKeyPair(fr); + } + } + else + { + KeyPair domainKeyPair = KeyPairUtils.createKeyPair(KEY_SIZE); + + try (FileWriter fw = new FileWriter(file)) + { + KeyPairUtils.writeKeyPair(domainKeyPair, fw); + } + + return domainKeyPair; + } + } + + /** + * Serializes user account into the given file. + * + * @param file + * The given file to store the client registration. + */ + private static void serializeUserAccount(URI regAccount, File file) + { + try (ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream(file))) + { + oos.writeObject(regAccount); + oos.flush(); + } + catch(IOException ex) + { + LOG.info("Can't save the client registration URI '" + regAccount + "'"); + } + } + + /** + * Reads a user account from the given file. + * + * @param file + * The given file with the client registration. + */ + private static URI readUserAccount(File file) + { + URI regAccount = null; + + try (ObjectInputStream ois = new ObjectInputStream(new FileInputStream(file))) + { + Object uri = ois.readObject(); + if (!(uri instanceof URI)) + { + throw new IOException("The file is corrupted"); + } + + regAccount = (URI) uri; + } + catch(IOException | ClassNotFoundException ex) + { + LOG.info("Can't get the client registration URI from " + file.toString() + "'", ex); + } + + return regAccount; + } + + /** + * 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 + * @throws Exception + */ + private void authorize(String domain) throws AcmeException + { + // Authorize the domain. + Authorization auth = registration.authorizeDomain(domain); + LOG.info("Authorization for domain " + domain); + + // Find the desired challenge and prepare it. + Challenge challenge = tlsSniChallenge(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; + } + + if (!acceptTlsSniChallenge(((TlsSni01Challenge) challenge).getSubject(), + ACCEPT_CHALLENGE_TIMEOUT)) + { + throw new AcmeException("Challenge is not accepted"); + } + + // Now trigger the challenge. + challenge.trigger(); + + // Poll for the challenge to complete. + try + { + int attempts = CHALLENGE_TRIES; + 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(UPDATE_CHALLENGE_TIMEOUT); + + // 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 TLS-SNI challenge. + *

+ * The verification of this challenge expects that the web server returns a special + * validation certificate. + *

+ * This example outputs instructions that need to be executed manually. In a + * production environment, you would rather configure your web server automatically. + * + * @param auth + * {@link Authorization} to find the challenge in + * @param domain + * Domain name to be authorized + * + * @return {@link Challenge} to verify + */ + @SuppressWarnings("deprecation") + // until tls-sni-02 is supported + public Challenge tlsSniChallenge(Authorization auth, String domain) throws AcmeException + { + // Find a single tls-sni-01 challenge + TlsSni01Challenge challenge = auth.findChallenge(TlsSni01Challenge.TYPE); + if (challenge == null) + { + throw new AcmeException("Found no " + TlsSni01Challenge.TYPE + + " challenge, don't know what to do..."); + } + + // Get the Subject + String subject = challenge.getSubject(); + + // Create a validation key pair + KeyPair domainKeyPair; + try (FileWriter fw = new FileWriter("tlssni.key")) + { + domainKeyPair = KeyPairUtils.createKeyPair(2048); + KeyPairUtils.writeKeyPair(domainKeyPair, fw); + } + catch (IOException ex) + { + throw new AcmeException("Could not write keypair", ex); + } + + // Create a validation certificate + try (FileWriter fw = new FileWriter("tlssni.crt")) + { + X509Certificate cert = CertificateUtils.createTlsSniCertificate(domainKeyPair, subject); + CertificateUtils.writeX509Certificate(cert, fw); + } + catch (IOException ex) + { + throw new AcmeException("Could not write certificate", ex); + } + + // Output the challenge, wait for acknowledge... + LOG.info("Please configure your web server."); + LOG.info("It must return the certificate 'tlssni.crt' on a SNI request to:"); + LOG.info(subject); + LOG.info("The matching keypair is available at 'tlssni.key'."); + LOG.info("If you're ready, dismiss the dialog..."); + + StringBuilder message = new StringBuilder(); + message.append("Please use 'tlssni.key' and 'tlssni.crt' cert for SNI requests to:\n\n"); + message.append("https://").append(subject).append("\n\n"); + LOG.info(message.toString()); + + return challenge; + } + + /** + * Prepares the challenge validation that starts the managed web server for the provided + * subject. + * + * @param subject + * The provided subject + * @param timeout + * The waiting time until the server is ready or failed. + */ + public boolean acceptTlsSniChallenge(String subject, long timeout) + throws AcmeException + { + try + { + mws = new ManagedWebServer(host, port, subject); + + return mws.waitUntilReady(timeout); + } + catch (Exception e) + { + throw new AcmeException("Managed web server " + host + ":" + port + " can't be started", + e); + } + } + + /** + * Shutdown the managed web server. + */ + public void shutdown() + { + if (mws != null) + { + try + { + mws.shutdown(); + } + catch (Exception e) + { + } + } + } + + /** + * Invokes this example. + * + * @param args + * Domains to get a certificate for + */ + public static void main(String... args) + { + InputParameters inputParameters = new InputParameters(); + + CmdLineParser parser = new CmdLineParser(inputParameters); + + try + { + parser.parseArgument(args); + + if (inputParameters.serverUri == null) + { + throw new CmdLineException(parser, + "There are no input parameters", + new IllegalArgumentException()); + } + } + catch (CmdLineException e) + { + System.err.println(e.getMessage()); + parser.printUsage(System.err); + return; + } + + LOG.info("Starting up..."); + + Security.addProvider(new BouncyCastleProvider()); + + Collection domains = Arrays.asList(inputParameters.domains.split(" ")); + + AcmeClient client = new AcmeClient(inputParameters.serverUri, + inputParameters.host, + inputParameters.port); + try + { + KeyPair userKeyPair = loadOrCreateKeyPair(USER_KEY_FILE); + + URI regAccount = readUserAccount(USER_REG_FILE); + + if (regAccount == null) + { + regAccount = client.registerAccount(userKeyPair, Collections.emptyList()); + serializeUserAccount(regAccount, USER_REG_FILE); + } + else + { + client.loginAccount(userKeyPair, regAccount); + } + + LOG.info("The client account is " + regAccount); + + client.askCertificate(Collections.emptyMap(), domains); + } + catch (Exception ex) + { + LOG.error("Failed to get a certificate for domains " + domains, ex); + } + + client.shutdown(); + } + + /** + * Defines the ACME client input parameters. + */ + static class InputParameters + { + /** + * The domain host address. Can be set via "-host" option. + */ + @Option(name="-host", + usage="The domain host address.") + public String host; + + /** + * The internal redirected port or 443 standard HTTPS port. Can be set via "-port" option. + * It must be 443 only or the redirected port number. ACME client must be available + * for external ACME servers via the requested domain name and the port number 443. + */ + @Option(name="-port", + usage="The domain port.") + public Integer port; + + /** + * The ACME server URI. Can be set via "-server" option. For the testing purpose it must be + * "acme://letsencrypt.org/staging". + */ + @Option(name="-server", + depends={"-host","-port", "-domains"}, + usage="The ACME server URI.") + public String serverUri; + + /** + * The list of requested domains separated by spaces that is similar to this example, + * "test1.acme.com test2.acme.com test3.acme.com". The wild cards are not supported. + */ + @Option(name="-domains", + usage="The requested domains enclosed in one string within double quoters and " + + "separated by spaces.") + public String domains; + } + +} === added file 'src/com/goldencode/p2j/security/ManagedWebServer.java' --- src/com/goldencode/p2j/security/ManagedWebServer.java 1970-01-01 00:00:00 +0000 +++ src/com/goldencode/p2j/security/ManagedWebServer.java 2017-10-31 08:47:43 +0000 @@ -0,0 +1,373 @@ +/* +** Module : ManagedWebServer.java +** Abstract : Implements Managed Web Server to prove the ownership of the target domain. +** +** Copyright (c) 2017, Golden Code Development Corporation. +** +** -#- -I- --Date-- ---------------------------------Description---------------------------------- +** 001 SBI 20171030 First version. +*/ +/* +** This program is free software: you can redistribute it and/or modify +** it under the terms of the GNU Affero General Public License as +** published by the Free Software Foundation, either version 3 of the +** License, or (at your option) any later version. +** +** 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. See the +** GNU Affero General Public License for more details. +** +** You may find a copy of the GNU Affero GPL version 3 at the following +** location: https://www.gnu.org/licenses/agpl-3.0.en.html +** +** Additional terms under GNU Affero GPL version 3 section 7: +** +** Under Section 7 of the GNU Affero GPL version 3, the following additional +** terms apply to the works covered under the License. These additional terms +** are non-permissive additional terms allowed under Section 7 of the GNU +** Affero GPL version 3 and may not be removed by you. +** +** 0. Attribution Requirement. +** +** You must preserve all legal notices or author attributions in the covered +** work or Appropriate Legal Notices displayed by works containing the covered +** work. You may not remove from the covered work any author or developer +** credit already included within the covered work. +** +** 1. No License To Use Trademarks. +** +** This license does not grant any license or rights to use the trademarks +** Golden Code, FWD, any Golden Code or FWD logo, or any other trademarks +** of Golden Code Development Corporation. You are not authorized to use the +** name Golden Code, FWD, or the names of any author or contributor, for +** publicity purposes without written authorization. +** +** 2. No Misrepresentation of Affiliation. +** +** You may not represent yourself as Golden Code Development Corporation or FWD. +** +** You may not represent yourself for publicity purposes as associated with +** Golden Code Development Corporation, FWD, or any author or contributor to +** the covered work, without written authorization. +** +** 3. No Misrepresentation of Source or Origin. +** +** You may not represent the covered work as solely your work. All modified +** versions of the covered work must be marked in a reasonable way to make it +** clear that the modified work is not originating from Golden Code Development +** Corporation or FWD. All modified versions must contain the notices of +** attribution required in this license. +*/ + + +package com.goldencode.p2j.security; + +import java.io.*; +import java.security.*; +import java.security.cert.*; +import java.security.cert.Certificate; +import java.util.concurrent.*; + +import org.eclipse.jetty.client.*; +import org.eclipse.jetty.client.api.*; +import org.eclipse.jetty.client.api.Response.CompleteListener; +import org.eclipse.jetty.http.*; +import org.eclipse.jetty.server.handler.*; +import org.eclipse.jetty.util.ssl.*; +import org.kohsuke.args4j.*; + +import com.goldencode.p2j.web.*; +import com.goldencode.util.*; + + + +/** + * Represents the functionality to be able to start an instance of the managed web server that + * responds on SNI requests to the subject provided by the ACME server in order to prove + * the ownership of this host. + */ +public class ManagedWebServer +{ + /** The request timeout */ + private static final long REQUEST_TIMEOUT = 1000; + + /** The file name of the server private certificate */ + private static final String SERVER_KEY_FILE = "tlssni.key"; + + /** The file name of the server public certificate */ + private static final String SERVER_CRT_FILE = "tlssni.crt"; + + /** The server alias */ + private static final String SERVER_ALIAS = "managed-server"; + + /** The server key store */ + private static final String SERVER_STORE = "managed-server.store"; + + /** The secure web server */ + private final SecureWebServer server; + + /** The private key store */ + private final KeyStore keyStore; + + /** The public key store */ + private final KeyStore certStore; + + /** The certificate factory */ + private SSLCertFactory factory; + + /** The http client */ + private HttpClient client; + + /** Indicates if the server is ready to respond on SNI requests to the subject. */ + private boolean isReady; + + /** + * Starts an instance of the managed https web server that responds on SNI requests to the + * subject provided by the ACME server in order to prove the ownership of this host. + * + * @param host + * The host address + * @param port + * The available host port + * @param subject + * The provided virtual host name by ACME server in order to prove the ownership of + * this host. It can be written to follow this host name pattern + * "f7ea254ebdbfc4a41637cb08294f3721.772069a9f4ff4bf7cc88bc40ba9f01b2.acme.invalid". + * + * @throws Exception + * Iff at least one of the web server and its client is failed to start. + */ + public ManagedWebServer(String host, int port, String subject) throws Exception + { + factory = new BCCertFactory(); + + keyStore = SSLCertGenUtil.createEmptyStore(); + + certStore = SSLCertGenUtil.createEmptyStore(); + + factory.loadRootCA(SERVER_CRT_FILE, SERVER_KEY_FILE, ""); + + // key entry password + String keyEntryPassword = factory.saveRootCA(SERVER_ALIAS, certStore, keyStore); + + try + { + Key rootKey = keyStore.getKey(SERVER_ALIAS, keyEntryPassword.toCharArray()); + Certificate rootCert = certStore.getCertificate(SERVER_ALIAS); + + KeyStore store = SSLCertGenUtil.createEmptyStore(); + store.setKeyEntry(SERVER_ALIAS, rootKey, keyEntryPassword.toCharArray(), + new Certificate[] { rootCert }); + + String keyStorePassword = RandomWordGenerator.create(); + store.store(new FileOutputStream(SERVER_STORE), keyStorePassword.toCharArray()); + + server = new SecureWebServer(SERVER_STORE, keyStorePassword, keyEntryPassword); + } + catch (UnrecoverableKeyException | + KeyStoreException | + NoSuchAlgorithmException | + CertificateException | + IOException e) + { + throw new SSLCertGenException(e); + } + + ResourceHandler rootHandler = new ResourceHandler(); + rootHandler.setDirectoriesListed(false); + + ContextHandler rootContext = new ContextHandler(); + rootContext.setContextPath("/"); + rootContext.setHandler(rootHandler); + + rootContext.addVirtualHosts( + new String[] {subject}); + + ContextHandler handler = new ContextHandler("/"); + handler.setHandler(rootContext); + server.addHandler(handler); + server.startup(host, port); + + SslContextFactory sslContextFactory = new SslContextFactory(); + + sslContextFactory.setTrustStore(certStore); + + client = new HttpClient(sslContextFactory); + client.start(); + } + + /** + * Waits if the server is ready or the elapsed time exceeds the given timeout until the first + * event occurs. + * + * @param timeout + * The given timeout + * + * @return True iff the server is ready. + */ + public boolean waitUntilReady(long timeout) + { + isReady = false; + + CountDownLatch ready = new CountDownLatch(1); + + CompleteListener callback = new CompleteListener() + { + + @Override + public void onComplete(Result result) + { + isReady = !result.isFailed(); + if (isReady) + { + ready.countDown(); + } + } + }; + + long startWaiting = System.currentTimeMillis(); + + long elapsedTime = 0; + + while(!isReady && ready.getCount() == 1) + { + isServerReady(callback, REQUEST_TIMEOUT); + try + { + ready.await(REQUEST_TIMEOUT, TimeUnit.MILLISECONDS); + } + catch (InterruptedException e) + { + } + finally + { + elapsedTime = System.currentTimeMillis() - startWaiting; + } + + if (elapsedTime >= timeout) + { + ready.countDown(); + } + } + + return isReady; + } + + /** + * Tests asynchronously if the target server is ready. + * + * @param callback + * The given callback + * @param timeout + * The https request timeout + */ + private void isServerReady(CompleteListener callback, long timeout) + { + StringBuilder connectTest = new StringBuilder(); + connectTest.append(HttpScheme.HTTPS.asString()) + .append("://") + .append(server.getHost()).append(":") + .append(server.getPort()).append("/"); + client.newRequest(connectTest.toString()) + .timeout(timeout, TimeUnit.MILLISECONDS) + .send(callback); + } + + /** + * Stops the web server and its client. + * + * @throws Exception + * iff the shutdown operation is failed + */ + public void shutdown() + throws Exception + { + server.shutdown(); + server.join(); + client.stop(); + } + + /** + * Starts the managed web server from the given command line arguments following this pattern: + * -host "127.0.0.1" -port 8888 -subject "test.acme.invalid". + * + * @param args + * The provided parameters + */ + public static void main(String[] args) + { + InputParameters inputParameters = new InputParameters(); + + CmdLineParser parser = new CmdLineParser(inputParameters); + + try + { + parser.parseArgument(args); + } + catch (CmdLineException e) + { + System.err.println(e.getMessage()); + parser.printUsage(System.err); + return; + } + + try + { + + ManagedWebServer mws = new ManagedWebServer(inputParameters.host, + inputParameters.port, + inputParameters.subject); + + boolean testResult = mws.waitUntilReady(10000); + + String msg; + + if (testResult) + { + msg = "The server is started successfully."; + } + else + { + msg = "The server is failed to start."; + } + + System.out.println(msg); + + if (!testResult) + { + System.exit(-1); + } + } + catch (Exception e) + { + e.printStackTrace(); + } + } + + /** + * Defines the managed web server input parameters. + */ + static class InputParameters + { + /** + * The server host address. Can be set via "-host" option. + */ + @Option(name="-host", + usage="The server host address.") + public String host; + + /** + * The server port. Can be set via "-port" option. + */ + @Option(name="-port", usage="The server port.") + public Integer port; + + /** + * The provided subject. Can be set via "-subject" option. + */ + @Option(name="-subject", + usage="The provided subject.") + public String subject; + } +} === modified file 'src/com/goldencode/p2j/security/SSLCertGenUtil.java' --- src/com/goldencode/p2j/security/SSLCertGenUtil.java 2017-10-30 14:18:34 +0000 +++ src/com/goldencode/p2j/security/SSLCertGenUtil.java 2017-10-30 23:47:56 +0000 @@ -1618,7 +1618,7 @@ * @throws SSLCertGenException * If the store could not be generated. */ - private KeyStore createEmptyStore() + static KeyStore createEmptyStore() throws SSLCertGenException { try