# 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;
       }
    }