ManagedWebServer.java
/*
** Module : ManagedWebServer.java
** Abstract : Implements Managed Web Server to prove the ownership of the target domain.
**
** Copyright (c) 2017-2023, Golden Code Development Corporation.
**
** -#- -I- --Date-- ---------------------------------Description----------------------------------
** 001 SBI 20171030 First version.
** 002 SBI 20171112 Added CertificateSuite usage.
** 003 TJD 20220331 Replaced deprecated new SslContextFactory() with SslContextFactory.Server()
** 004 GBB 20230512 Logging methods replaced by CentralLogger/ConversionStatus.
*/
/*
** 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 com.goldencode.p2j.util.logging.*;
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.security.SSLCertFactory.*;
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";
/** Logger */
private static final CentralLogger LOG = CentralLogger.get(ManagedWebServer.class);
/** 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();
CertificateSuite suite = factory.loadCertificateSuite(SERVER_CRT_FILE, SERVER_KEY_FILE, null);
try
{
Key rootKey = suite.privateKey;
Certificate rootCert = suite.publicKey;
// key entry password
String keyEntryPassword = RandomWordGenerator.create();
keyStore.setKeyEntry(SERVER_ALIAS, rootKey, keyEntryPassword.toCharArray(),
new Certificate[] { rootCert });
// key store password
String keyStorePassword = RandomWordGenerator.create();
keyStore.store(new FileOutputStream(SERVER_STORE), keyStorePassword.toCharArray());
certStore.setCertificateEntry(SERVER_ALIAS, suite.publicKey);
server = new SecureWebServer(SERVER_STORE, keyStorePassword, keyEntryPassword);
}
catch (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.Server();
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)
{
LOG.severe("", e);
StringOutputStream outputStream = new StringOutputStream();
parser.printUsage(outputStream);
LOG.info(outputStream.getData());
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)
{
LOG.severe("", e);
}
}
/**
* 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;
}
}