Constants.java
package com.renomad.minum.state;
import com.renomad.minum.logging.LoggingLevel;
import com.renomad.minum.utils.TimeUtils;
import java.io.FileInputStream;
import java.io.IOException;
import java.util.*;
/**
* Very important system design decisions are made here. All
* developers on this project should look through each of these.
*/
public final class Constants {
private final Properties properties;
public Constants() {
this(null);
}
public Constants(Properties props) {
properties = Objects.requireNonNullElseGet(props, Constants::getConfiguredProperties);
serverPort = getProp("SERVER_PORT", 8080);
secureServerPort = getProp("SSL_SERVER_PORT", 8443);
hostName = properties.getProperty("HOST_NAME", "localhost");
dbDirectory = properties.getProperty("DB_DIRECTORY", "db");
staticFilesDirectory = properties.getProperty("STATIC_FILES_DIRECTORY", "static");
logLevels = convertLoggingStringsToEnums(getProp("LOG_LEVELS", "DEBUG,TRACE,ASYNC_ERROR,AUDIT"));
keystorePath = properties.getProperty("KEYSTORE_PATH", "");
keystorePassword = properties.getProperty("KEYSTORE_PASSWORD", "");
maxReadSizeBytes = getProp("MAX_READ_SIZE_BYTES", 10 * 1024 * 1024);
maxReadLineSizeBytes = getProp("MAX_READ_LINE_SIZE_BYTES", 1024);
socketTimeoutMillis = getProp("SOCKET_TIMEOUT_MILLIS", 7 * 1000);
keepAliveTimeoutSeconds = getProp("KEEP_ALIVE_TIMEOUT_SECONDS", 3);
vulnSeekingJailDuration = getProp("VULN_SEEKING_JAIL_DURATION", 10 * 1000);
isTheBrigEnabled = getProp("IS_THE_BRIG_ENABLED", true);
suspiciousErrors = getProp("SUSPICIOUS_ERRORS", "");
suspiciousPaths = getProp("SUSPICIOUS_PATHS", "");
startTime = System.currentTimeMillis();
extraMimeMappings = getProp("EXTRA_MIME_MAPPINGS", "");
staticFileCacheTime = getProp("STATIC_FILE_CACHE_TIME", 60 * 5);
useCacheForStaticFiles = getProp("USE_CACHE_FOR_STATIC_FILES", true);
maxElementsLruCacheStaticFiles = getProp("MAX_ELEMENTS_LRU_CACHE_STATIC_FILES", 1000);
}
/**
* The port for our regular, non-encrypted server
*/
public final int serverPort;
/**
* The port for our encrypted server
*/
public final int secureServerPort;
/**
* This is returned as the "host:" attribute in an HTTP request
*/
public final String hostName;
/**
* This is the root directory of our database
*/
public final String dbDirectory;
/**
* Root directory of static files
*/
public final String staticFilesDirectory;
/**
* The default logging levels
*/
public final List<LoggingLevel> logLevels;
/**
* The path to the keystore, required for encrypted TLS communication
*/
public final String keystorePath;
/**
* The password of the keystore, used for TLS
*/
public final String keystorePassword;
/**
* this is the most bytes we'll read while parsing the Request body
*/
public final int maxReadSizeBytes;
/**
* this is the most bytes we'll read on a single line, when reading by
* line. This is especially relevant when reading headers and request lines, which
* can bulk up with jwt's or query strings, respectively.
*/
public final int maxReadLineSizeBytes;
/**
* How long will we let a socket live before we crash it closed?
* See {@link java.net.Socket#setSoTimeout(int)}
*/
public final int socketTimeoutMillis;
/**
* We include this value in the keep-alive header. It lets the
* browser know how long to hold the socket open, in seconds,
* before it decides we aren't sending anything else and closes it.
*/
public final int keepAliveTimeoutSeconds;
/**
* If a client does something that we consider an indicator for attacking, put them in
* jail for a longer duration.
*/
public final int vulnSeekingJailDuration;
/**
* TheBrig is what puts client ip's in jail, if we feel they are attacking us.
* If this is disabled, that functionality is removed.
*/
public final boolean isTheBrigEnabled;
/**
* These are a list of error messages that often indicate unusual behavior, maybe an attacker
*/
public final List<String> suspiciousErrors;
/**
* These are a list of paths that often indicate unusual behavior, maybe an attacker
*/
public final List<String> suspiciousPaths;
/**
* This value is the result of running System.currentTimeMillis() when this
* class gets instantiated, and that is done at the very beginning.
*/
public final long startTime;
/**
* These are key-value pairs for mappings between a file
* suffix and a mime type.
* <p>
* These are read by our system in the StaticFilesCache
* as key-1,value-1,key-2,value-2,... and so on.
* </p>
* @see <a href="https://developer.mozilla.org/en-US/docs/Web/HTTP/Basics_of_HTTP/MIME_types">Basics of HTTP</a>
*
*/
public final List<String> extraMimeMappings;
/**
* Length of time, in seconds, for static files to be cached,
* per the provisions of the Cache-Control header, e.g.
* <pre>
* {@code Cache-Control: max-age=300}
* </pre>
*/
public final long staticFileCacheTime;
/**
* Whether we will use caching for the static files.
* <p>
* When a user requests a path we don't recognize, we
* go looking for it.
* If we have already found it for someone else, it will
* be in a cache.
* </p>
* <p>
* However, if we are doing development, it helps to
* not have caching enabled - it can confuse.
* </p>
*/
public final boolean useCacheForStaticFiles;
/**
* This constant controls the maximum number of elements for the {@link com.renomad.minum.utils.LRUCache}
* we create for use by {@link com.renomad.minum.utils.FileUtils}. As files are read
* by FileUtil's methods, they will be stored in this cache, to avoid reading from
* disk. However, caching can certainly complicate things, so if you would prefer
* not to store these values in a cache, set {@link #useCacheForStaticFiles} to false.
* <p>
* The unit here is the number of elements to store in the cache. Be aware: elements
* can be of any size, so two caches each having a max size of 1000 elements could be
* drastically different sizes.
* </p>
*/
public final int maxElementsLruCacheStaticFiles;
/**
* A helper method to remove some redundant boilerplate code for grabbing
* configuration values from minum.config
*/
private int getProp(String propName, int propDefault) {
return Integer.parseInt(properties.getProperty(propName, String.valueOf(propDefault)));
}
/**
* A helper method to remove some redundant boilerplate code for grabbing
* configuration values from minum.config
*/
private boolean getProp(String propName, boolean propDefault) {
return Boolean.parseBoolean(properties.getProperty(propName, String.valueOf(propDefault)));
}
/**
* A helper method to remove some redundant boilerplate code for grabbing
* configuration values from minum.config
*/
private List<String> getProp(String propName, String propDefault) {
String propValue = properties.getProperty(propName);
return extractList(propValue, propDefault);
}
/**
* Extract a list out of a comma-delimited string.
* <br>
* Example: a,b, c, d -> List.of("a","b","c","d")
* @param propValue the value of a property
* @param propDefault the default value to use, if the propValue is null
*/
static List<String> extractList(String propValue, String propDefault) {
if (propValue == null) {
if (propDefault.isBlank()) {
return List.of();
} else {
return Arrays.asList(propDefault.trim().split("\\s*,\\s*"));
}
} else {
return Arrays.asList(propValue.trim().split("\\s*,\\s*"));
}
}
public static Properties getConfiguredProperties() {
return getConfiguredProperties("minum.config");
}
/**
* Reads properties from minum.config
*/
static Properties getConfiguredProperties(String fileName) {
var props = new Properties();
try (FileInputStream fis = new FileInputStream(fileName)) {
System.out.println(TimeUtils.getTimestampIsoInstant() +
" found properties file at ./minum.config. Loading properties");
props.load(fis);
} catch (IOException ex) {
System.out.println(CONFIG_ERROR_MESSAGE);
}
return props;
}
/**
* Given a list of strings representing logging levels,
* convert it to a list of enums. Log levels are enumerated
* in {@link LoggingLevel}.
*/
static List<LoggingLevel> convertLoggingStringsToEnums(List<String> logLevels) {
List<String> logLevelStrings = logLevels.stream().map(String::toUpperCase).toList();
List<LoggingLevel> enabledLoggingLevels = new ArrayList<>();
for (LoggingLevel t : LoggingLevel.values()) {
if (logLevelStrings.contains(t.name())) {
enabledLoggingLevels.add(t);
}
}
return enabledLoggingLevels;
}
private static final String CONFIG_ERROR_MESSAGE = """
----------------------------------------------------------------
----------------- System Configuration Missing -----------------
----------------------------------------------------------------
No properties file found at ./minum.config
Continuing, using defaults. See source code for Minum for an
example minum.config, which will allow you to customize behavior.
----------------------------------------------------------------
----------------------------------------------------------------
""";
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
Constants constants = (Constants) o;
return serverPort == constants.serverPort && secureServerPort == constants.secureServerPort && maxReadSizeBytes == constants.maxReadSizeBytes && maxReadLineSizeBytes == constants.maxReadLineSizeBytes && socketTimeoutMillis == constants.socketTimeoutMillis && keepAliveTimeoutSeconds == constants.keepAliveTimeoutSeconds && vulnSeekingJailDuration == constants.vulnSeekingJailDuration && isTheBrigEnabled == constants.isTheBrigEnabled && startTime == constants.startTime && staticFileCacheTime == constants.staticFileCacheTime && useCacheForStaticFiles == constants.useCacheForStaticFiles && maxElementsLruCacheStaticFiles == constants.maxElementsLruCacheStaticFiles && Objects.equals(properties, constants.properties) && Objects.equals(hostName, constants.hostName) && Objects.equals(dbDirectory, constants.dbDirectory) && Objects.equals(staticFilesDirectory, constants.staticFilesDirectory) && Objects.equals(logLevels, constants.logLevels) && Objects.equals(keystorePath, constants.keystorePath) && Objects.equals(keystorePassword, constants.keystorePassword) && Objects.equals(suspiciousErrors, constants.suspiciousErrors) && Objects.equals(suspiciousPaths, constants.suspiciousPaths) && Objects.equals(extraMimeMappings, constants.extraMimeMappings);
}
@Override
public int hashCode() {
return Objects.hash(properties, serverPort, secureServerPort, hostName, dbDirectory, staticFilesDirectory, logLevels, keystorePath, keystorePassword, maxReadSizeBytes, maxReadLineSizeBytes, socketTimeoutMillis, keepAliveTimeoutSeconds, vulnSeekingJailDuration, isTheBrigEnabled, suspiciousErrors, suspiciousPaths, startTime, extraMimeMappings, staticFileCacheTime, useCacheForStaticFiles, maxElementsLruCacheStaticFiles);
}
}