| 1 | package com.renomad.minum.web; | |
| 2 | ||
| 3 | import com.renomad.minum.state.Constants; | |
| 4 | import com.renomad.minum.logging.ILogger; | |
| 5 | import com.renomad.minum.state.Context; | |
| 6 | ||
| 7 | import javax.net.ssl.KeyManagerFactory; | |
| 8 | import javax.net.ssl.SSLContext; | |
| 9 | import java.io.IOException; | |
| 10 | import java.io.InputStream; | |
| 11 | import java.net.ServerSocket; | |
| 12 | import java.net.Socket; | |
| 13 | import java.net.URL; | |
| 14 | import java.nio.file.Path; | |
| 15 | import java.security.*; | |
| 16 | import java.util.concurrent.ExecutorService; | |
| 17 | ||
| 18 | import static com.renomad.minum.web.HttpServerType.ENCRYPTED_HTTP; | |
| 19 | import static com.renomad.minum.web.HttpServerType.PLAIN_TEXT_HTTP; | |
| 20 | ||
| 21 | /** | |
| 22 | * This class contains the basic internet capabilities. | |
| 23 | * <br><br> | |
| 24 | * Think of this class as managing some of the lowest-level internet | |
| 25 | * communications we need to handle to support a web application. Sockets, | |
| 26 | * Servers, Threads, that kind of stuff. | |
| 27 | */ | |
| 28 | final class WebEngine { | |
| 29 | ||
| 30 | private final Constants constants; | |
| 31 | private final Context context; | |
| 32 | private final ExecutorService executorService; | |
| 33 | private final WebFramework webFramework; | |
| 34 | ||
| 35 | WebEngine(Context context, WebFramework webFramework) { | |
| 36 | this.logger = context.getLogger(); | |
| 37 | this.logger.logDebug(() -> "Using a supplied logger in WebEngine"); | |
| 38 | this.constants = context.getConstants(); | |
| 39 | this.context = context; | |
| 40 | this.executorService = context.getExecutorService(); | |
| 41 | this.webFramework = webFramework; | |
| 42 | } | |
| 43 | ||
| 44 | private final ILogger logger; | |
| 45 | static final String HTTP_CRLF = "\r\n"; | |
| 46 | ||
| 47 | /** | |
| 48 | * Start a plain-text unencrypted server, listening on the port specified by the user | |
| 49 | */ | |
| 50 | IServer startServer() { | |
| 51 | int port = constants.serverPort; | |
| 52 | ServerSocket ss; | |
| 53 | try { | |
| 54 | ss = new ServerSocket(port); | |
| 55 | } catch (Exception e) { | |
| 56 | throw new WebServerException("Failed to create serversocket on port " + port, e); | |
| 57 | } | |
| 58 | logger.logDebug(() -> String.format("Just created a new ServerSocket: %s", ss)); | |
| 59 | IServer server = new Server(ss, context, "http server", webFramework, executorService, PLAIN_TEXT_HTTP); | |
| 60 | logger.logDebug(() -> String.format("Just created a new Server: %s", server)); | |
| 61 |
1
1. startServer : removed call to com/renomad/minum/web/IServer::start → KILLED |
server.start(); |
| 62 | String hostname = constants.hostName; | |
| 63 | logger.logDebug(() -> String.format("%s started at http://%s:%s", server, hostname, port)); | |
| 64 |
1
1. startServer : replaced return value with null for com/renomad/minum/web/WebEngine::startServer → KILLED |
return server; |
| 65 | } | |
| 66 | ||
| 67 | /** | |
| 68 | * Start an encrypted server, using TLS 1.3 | |
| 69 | */ | |
| 70 | IServer startSslServer() { | |
| 71 | ||
| 72 | /* | |
| 73 | * If we are provided details for a keystore (its location and password), use it | |
| 74 | */ | |
| 75 | final var useExternalKeystore = isProvidedKeystoreProperties(constants.keystorePath, constants.keystorePassword, logger); | |
| 76 | KeyStoreResult keystoreResult = getKeyStoreResult(useExternalKeystore, constants.keystorePath, constants.keystorePassword, logger); | |
| 77 | ||
| 78 | int port = constants.secureServerPort; | |
| 79 | ServerSocket ss = createSslSocketWithSpecificKeystore(port, keystoreResult.keystoreUrl(), keystoreResult.keystorePassword()); | |
| 80 | logger.logDebug(() -> String.format("Just created a new ServerSocket: %s", ss)); | |
| 81 | IServer server = new Server(ss, context, "https server", webFramework, executorService, ENCRYPTED_HTTP); | |
| 82 | logger.logDebug(() -> String.format("Just created a new SSL Server: %s", server)); | |
| 83 |
1
1. startSslServer : removed call to com/renomad/minum/web/IServer::start → KILLED |
server.start(); |
| 84 | String hostname = constants.hostName; | |
| 85 | logger.logDebug(() -> String.format("%s started at https://%s:%s", server, hostname, port)); | |
| 86 |
1
1. startSslServer : replaced return value with null for com/renomad/minum/web/WebEngine::startSslServer → KILLED |
return server; |
| 87 | } | |
| 88 | ||
| 89 | static KeyStoreResult getKeyStoreResult( | |
| 90 | boolean useExternalKeystore, | |
| 91 | String keystorePath, | |
| 92 | String keystorePassword, | |
| 93 | ILogger logger) { | |
| 94 |
1
1. getKeyStoreResult : negated conditional → KILLED |
if (useExternalKeystore) { |
| 95 | logger.logDebug(() -> "Using keystore and password referenced in minum.config"); | |
| 96 | } else { | |
| 97 | logger.logDebug(() -> "Using the default (self-signed / testing-only) certificate"); | |
| 98 | } | |
| 99 | ||
| 100 | final URL keystoreUrl; | |
| 101 | try { | |
| 102 |
1
1. getKeyStoreResult : negated conditional → KILLED |
keystoreUrl = useExternalKeystore ? |
| 103 | Path.of(keystorePath).toUri().toURL() : | |
| 104 | WebEngine.class.getResource("/certs/keystore"); | |
| 105 | } catch (Exception e) { | |
| 106 | throw new WebServerException("Error while building keystoreUrl: " + e); | |
| 107 | } | |
| 108 |
1
1. getKeyStoreResult : negated conditional → KILLED |
final String keystorePasswordFinal = useExternalKeystore ? |
| 109 | keystorePassword : | |
| 110 | "passphrase"; | |
| 111 |
1
1. getKeyStoreResult : replaced return value with null for com/renomad/minum/web/WebEngine::getKeyStoreResult → KILLED |
return new KeyStoreResult(keystoreUrl, keystorePasswordFinal); |
| 112 | } | |
| 113 | ||
| 114 | record KeyStoreResult(URL keystoreUrl, String keystorePassword) { } | |
| 115 | ||
| 116 | ||
| 117 | /** | |
| 118 | * Look into the system properties to see whether values have been | |
| 119 | * set for the keystore and keystorePassword keys. | |
| 120 | * <p> | |
| 121 | * the key for keystore is: javax.net.ssl.keyStore | |
| 122 | * the key for keystorePassword is: javax.net.ssl.keyStorePassword | |
| 123 | * <p> | |
| 124 | * It's smart, if you are creating a server that will run | |
| 125 | * with a genuine signed certificate, to have those files | |
| 126 | * stored somewhere and then set these system properties. That | |
| 127 | * way, it's a characteristic of a particular server - it's not | |
| 128 | * needed to bundle the certificate with the actual server in | |
| 129 | * any way. | |
| 130 | * <p> | |
| 131 | * We *do* bundle a cert, but it's for testing and is self-signed. | |
| 132 | */ | |
| 133 | static Boolean isProvidedKeystoreProperties(String keystorePath, String keystorePassword, ILogger logger) { | |
| 134 | ||
| 135 | // get the directory to the keystore from a system property | |
| 136 |
2
1. isProvidedKeystoreProperties : negated conditional → KILLED 2. isProvidedKeystoreProperties : negated conditional → KILLED |
boolean hasKeystore = ! (keystorePath == null || keystorePath.isBlank()); |
| 137 |
1
1. isProvidedKeystoreProperties : negated conditional → KILLED |
if (! hasKeystore) { |
| 138 | logger.logDebug(() -> "Keystore system property was not set"); | |
| 139 | } | |
| 140 | ||
| 141 | // get the password to that keystore from a system property | |
| 142 |
2
1. isProvidedKeystoreProperties : negated conditional → KILLED 2. isProvidedKeystoreProperties : negated conditional → KILLED |
boolean hasKeystorePassword = ! (keystorePassword == null || keystorePassword.isBlank()); |
| 143 |
1
1. isProvidedKeystoreProperties : negated conditional → KILLED |
if (! hasKeystorePassword) { |
| 144 | logger.logDebug(() -> "keystorePassword system property was not set"); | |
| 145 | } | |
| 146 | ||
| 147 |
3
1. isProvidedKeystoreProperties : replaced Boolean return with True for com/renomad/minum/web/WebEngine::isProvidedKeystoreProperties → KILLED 2. isProvidedKeystoreProperties : negated conditional → KILLED 3. isProvidedKeystoreProperties : negated conditional → KILLED |
return hasKeystore && hasKeystorePassword; |
| 148 | } | |
| 149 | ||
| 150 | ||
| 151 | /** | |
| 152 | * Create an SSL Socket using a specified keystore | |
| 153 | */ | |
| 154 | ServerSocket createSslSocketWithSpecificKeystore(int sslPort, URL keystoreUrl, String keystorePassword) { | |
| 155 | try (InputStream keystoreInputStream = keystoreUrl.openStream()) { | |
| 156 | final var keyStore = KeyStore.getInstance(KeyStore.getDefaultType()); | |
| 157 | char[] passwordCharArray = keystorePassword.toCharArray(); | |
| 158 |
1
1. createSslSocketWithSpecificKeystore : removed call to java/security/KeyStore::load → KILLED |
keyStore.load(keystoreInputStream, passwordCharArray); |
| 159 | ||
| 160 | final var keyManagerFactory = KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm()); | |
| 161 |
1
1. createSslSocketWithSpecificKeystore : removed call to javax/net/ssl/KeyManagerFactory::init → KILLED |
keyManagerFactory.init(keyStore, passwordCharArray); |
| 162 | ||
| 163 | final var keyManagers = keyManagerFactory.getKeyManagers(); | |
| 164 | ||
| 165 | final var sslContext = SSLContext.getInstance("TLSv1.3"); | |
| 166 |
1
1. createSslSocketWithSpecificKeystore : removed call to javax/net/ssl/SSLContext::init → KILLED |
sslContext.init(keyManagers, null, new SecureRandom()); |
| 167 | ||
| 168 | final var socketFactory = sslContext.getServerSocketFactory(); | |
| 169 | return socketFactory.createServerSocket(sslPort); | |
| 170 | } catch (Exception ex) { | |
| 171 | String extraMessage = "Exception during creation of SSL socket with port %d, keystore URL of %s. Exception message: %s".formatted(sslPort, keystoreUrl, ex.getMessage()); | |
| 172 | logger.logDebug(() -> extraMessage); | |
| 173 | throw new WebServerException(extraMessage, ex); | |
| 174 | } | |
| 175 | } | |
| 176 | ||
| 177 | /** | |
| 178 | * Create a client {@link ISocketWrapper} connected to the running host server | |
| 179 | */ | |
| 180 | ISocketWrapper startClient(Socket socket) throws IOException { | |
| 181 | logger.logDebug(() -> String.format("Just created new client socket: %s", socket)); | |
| 182 |
1
1. startClient : replaced return value with null for com/renomad/minum/web/WebEngine::startClient → KILLED |
return new SocketWrapper(socket, null, logger, constants.socketTimeoutMillis, constants.hostName); |
| 183 | } | |
| 184 | ||
| 185 | } | |
Mutations | ||
| 61 |
1.1 |
|
| 64 |
1.1 |
|
| 83 |
1.1 |
|
| 86 |
1.1 |
|
| 94 |
1.1 |
|
| 102 |
1.1 |
|
| 108 |
1.1 |
|
| 111 |
1.1 |
|
| 136 |
1.1 2.2 |
|
| 137 |
1.1 |
|
| 142 |
1.1 2.2 |
|
| 143 |
1.1 |
|
| 147 |
1.1 2.2 3.3 |
|
| 158 |
1.1 |
|
| 161 |
1.1 |
|
| 166 |
1.1 |
|
| 182 |
1.1 |