diff --git a/server/bootstrap/src/org/labkey/bootstrap/LabKeyBootstrapClassLoader.java b/server/bootstrap/src/org/labkey/bootstrap/LabKeyBootstrapClassLoader.java index 360b394f7f..22df0109bc 100644 --- a/server/bootstrap/src/org/labkey/bootstrap/LabKeyBootstrapClassLoader.java +++ b/server/bootstrap/src/org/labkey/bootstrap/LabKeyBootstrapClassLoader.java @@ -47,14 +47,9 @@ public class LabKeyBootstrapClassLoader extends WebappClassLoader implements Exp private final Set _previouslyLoggedModules = new HashSet<>(); private final ReentrantLock moduleLoading = new ReentrantLock(); - // IMPORTANT see also ContextListener which duplicates this code, keep them consistent - // On startup on some platforms, some modules will die if java.awt.headless is not set to false. - // Only set this if the user hasn't overridden it static { - String headless = "java.awt.headless"; - if (System.getProperty(headless) == null) - System.setProperty(headless, "true"); + StartupEnvironment.ensureHeadless(); } private ModuleExtractor _moduleExtractor; diff --git a/server/bootstrap/src/org/labkey/bootstrap/StartupEnvironment.java b/server/bootstrap/src/org/labkey/bootstrap/StartupEnvironment.java new file mode 100644 index 0000000000..c38e459993 --- /dev/null +++ b/server/bootstrap/src/org/labkey/bootstrap/StartupEnvironment.java @@ -0,0 +1,58 @@ +/* + * Copyright (c) 2026 LabKey Corporation + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.labkey.bootstrap; + +import java.io.File; + +/** + * JVM-wide settings that have to be established before the rest of the server loads, shared by the entry points that + * might get there first. + */ +public class StartupEnvironment +{ + /** Directory the server was launched from, and where labkeyBootstrap.jar and ./logs live. */ + public static final String LABKEY_HOME_PROPERTY_NAME = "labkey.home"; + + /** Servlet context parameter that the embedded boot layer sets from context.bypass2FA */ + public static final String BYPASS_2FA_PARAMETER = "bypass2FA"; + + private static final String HEADLESS_PROPERTY_NAME = "java.awt.headless"; + + private StartupEnvironment() + { + } + + /** Some modules die during startup on some platforms if AWT isn't headless. */ + public static void ensureHeadless() + { + if (System.getProperty(HEADLESS_PROPERTY_NAME) == null) + System.setProperty(HEADLESS_PROPERTY_NAME, "true"); + } + + public static String ensureLabKeyHomeSet(File directory) + { + if (System.getProperty(LABKEY_HOME_PROPERTY_NAME) == null) + System.setProperty(LABKEY_HOME_PROPERTY_NAME, directory.getAbsolutePath()); + return System.getProperty(LABKEY_HOME_PROPERTY_NAME); + } + + /** Null before the boot layer has run, which is the case in tools that bootstrap their own JVM. */ + public static File getLabKeyHome() + { + String path = System.getProperty(LABKEY_HOME_PROPERTY_NAME); + return path == null ? null : new File(path); + } +} diff --git a/server/embedded/build.gradle b/server/embedded/build.gradle index 27e9501ad3..4796709667 100644 --- a/server/embedded/build.gradle +++ b/server/embedded/build.gradle @@ -41,7 +41,8 @@ dependencies { implementation "com.fasterxml.jackson.core:jackson-databind:${jacksonDatabindVersion}" implementation "com.fasterxml.jackson.core:jackson-annotations:${jacksonAnnotationsVersion}" - implementation("org.springframework.boot:spring-boot-starter-web:${springBootVersion}") { + // Pull in Tomcat as a servlet container, without Spring MVC + implementation("org.springframework.boot:spring-boot-starter-tomcat:${springBootVersion}") { exclude group: "jakarta.annotation", module: "jakarta.annotation-api" // Already present in tomcat-annotations-api exclude group: "org.apache.tomcat.embed", module: "tomcat-embed-core" // We want to force apacheTomcatVersion } @@ -94,11 +95,6 @@ dependencies { strictly "${springVersion}" } } - implementation('org.springframework:spring-webmvc') { - version { - strictly "${springVersion}" - } - } // Allows forcing a Tomcat version that differs from spring-boot's version (e.g., to address CVEs or regressions, // or to test a Tomcat release candidate) @@ -132,6 +128,7 @@ dependencies { exclude group: 'commons-logging', module: 'commons-logging' } + // sso/ssooidc are loaded reflectively for SSO-profile credentials, by this layer's SSM config and by cloudServices, which ships no SDK of its own implementation "software.amazon.awssdk:ssm:${awsSdkVersion}" implementation "software.amazon.awssdk:sso:${awsSdkVersion}" implementation "software.amazon.awssdk:ssooidc:${awsSdkVersion}" diff --git a/server/embedded/src/main/resources/labkey-defaults.properties b/server/embedded/src/main/resources/labkey-defaults.properties new file mode 100644 index 0000000000..4f2849b8a3 --- /dev/null +++ b/server/embedded/src/main/resources/labkey-defaults.properties @@ -0,0 +1,70 @@ +# Defaults for the embedded server. LabKeyServer.main() loads these as Spring's default properties, below every other +# source, so they apply even when spring.config.location replaces the standard search path and anything here can be +# overridden without editing the boot jar. Keep runtime-derived values (anything read from a system property or +# computed at startup) in LabKeyServer.main() instead -- this file is static text. + +server.tomcat.basedir=. +server.tomcat.accesslog.directory=${labkey.log.home} + +# Boost limits imposed by Tomcat v10.1.42 +server.tomcat.max-part-count=500 +# GitHub Issue 161: LKS insert forms can't handle long file field names +server.tomcat.max-part-header-size=1024 +server.tomcat.max-connections=1024 +# Boost limit back to Tomcat 10 level +server.tomcat.max-parameter-count=10000 + +# Enable HTTP compression for response content. Spring Boot compresses HTML, JSON and other types by default, but not +# TSV, CSV, or SVG, so we have to duplicate the defaults and add those types. +server.compression.enabled=true +server.compression.mime-types=text/html,text/xml,text/plain,text/css,text/javascript,application/javascript,application/json,application/xml,text/tab-separated-values,text/csv,image/svg+xml + +server.tomcat.accesslog.enabled=true +server.tomcat.accesslog.pattern=%h %l %u %t "%r" %s %b %D %S %I "%{Referer}i" "%{User-Agent}i" %{LABKEY.username}s %{X-Forwarded-For}i +jsonaccesslog.pattern=%h %t %m %U %s %b %D %S "%{Referer}i" "%{User-Agent}i" %{LABKEY.username}s %{X-Forwarded-For}i + +# A strong Content Security Policy. The ${...} substitutions other than context.contextPath are resolved by LabKey's +# CSP filter at request time, not by Spring; unresolvable placeholders are left intact during binding. +csp.enforce=\ + default-src 'self' ;\n\ + connect-src 'self' ${CONNECTION.SOURCES} ;\n\ + object-src ${OBJECT.SOURCES} ; /* Substitution value defaults to 'none' unless overridden by an admin */\n\ + style-src 'self' 'unsafe-inline' ${STYLE.SOURCES} ;\n\ + img-src 'self' data: ${IMAGE.SOURCES} ;\n\ + font-src 'self' data: ${FONT.SOURCES} ;\n\ + script-src 'unsafe-eval' 'strict-dynamic' 'nonce-${REQUEST.SCRIPT.NONCE}' ${SCRIPT.SOURCES} ;\n\ + base-uri 'self' ;\n\ + frame-src 'self' ${FRAME.SOURCES} ;\n\ + report-uri ${context.contextPath:}/admin-contentSecurityPolicyReport.api ;\n\ + ${UPGRADE.INSECURE.REQUESTS}\n\ + frame-ancestors 'self' ${FRAMEANCESTORS.SOURCES} ;\n\ + /* cspVersion=e16 */\n + +# The report policy leaves out upgrade_insecure_requests and frame-ancestors, since they produce warnings on some browsers +csp.report=\ + default-src 'self' ;\n\ + connect-src 'self' ${CONNECTION.SOURCES} ;\n\ + object-src ${OBJECT.SOURCES} ; /* Substitution value defaults to 'none' unless overridden by an admin */\n\ + style-src 'self' 'unsafe-inline' ${STYLE.SOURCES} ;\n\ + img-src 'self' data: ${IMAGE.SOURCES} ;\n\ + font-src 'self' data: ${FONT.SOURCES} ;\n\ + script-src 'unsafe-eval' 'strict-dynamic' 'nonce-${REQUEST.SCRIPT.NONCE}' ${SCRIPT.SOURCES} ;\n\ + base-uri 'self' ;\n\ + frame-src 'self' ${FRAME.SOURCES} ;\n\ + report-uri ${context.contextPath:}/admin-contentSecurityPolicyReport.api ;\n\ + /* cspVersion=r16 */\n + +# GitHub Issue 692: Stop using CBC in HTTPS ciphers. These settings configure HTTPS; admins must opt in with additional +# settings, like the key store. Without those, HTTP-only startup fails unless server.ssl.enabled is explicitly false. +server.ssl.enabled=false +server.ssl.protocol=TLS +server.ssl.enabled-protocols=TLSv1.3,TLSv1.2 +# Explicit JSSE cipher suite names, to avoid CBC-mode suites +server.ssl.ciphers=TLS_AES_256_GCM_SHA384,TLS_AES_128_GCM_SHA256,TLS_CHACHA20_POLY1305_SHA256,TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384,TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256,TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305_SHA256,TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384,TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256,TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305_SHA256 +server.ssl.use-cipher-suites-order=true + +# GitHub Issue #1416 - default values for SMTP timeouts +mail.smtpConnectionTimeout=10000 +mail.smtpTimeout=60000 +# Unlike the socket-level timeouts above, JavaMail implements writetimeout with a ScheduledThreadPool per connection - one per message, since Transport.send() doesn't pool +mail.smtpWriteTimeout=60000 diff --git a/server/embedded/src/org/labkey/embedded/EmbeddedExtractor.java b/server/embedded/src/org/labkey/embedded/EmbeddedExtractor.java index 12d3fed8c9..7176b7bcd1 100644 --- a/server/embedded/src/org/labkey/embedded/EmbeddedExtractor.java +++ b/server/embedded/src/org/labkey/embedded/EmbeddedExtractor.java @@ -20,6 +20,7 @@ import org.apache.logging.log4j.Logger; import org.labkey.bootstrap.ConfigException; import org.labkey.bootstrap.ModuleArchive; +import org.labkey.bootstrap.StartupEnvironment; import java.io.BufferedOutputStream; import java.io.File; @@ -239,7 +240,8 @@ public void extractDistribution(File webAppLocation) @SuppressWarnings("unused") /* Called via reflection by PipelineServiceImpl.getClusterStartupArguments() */ public File extractRemotePipelineJars() { - return extractExecutableJar(new File("."), false, true); + File labkeyHome = StartupEnvironment.getLabKeyHome(); + return extractExecutableJar(labkeyHome == null ? new File(".") : labkeyHome, false, true); } public void extractExecutableJar(File destDirectory, boolean remotePipeline) @@ -451,4 +453,4 @@ public String toString() { return distributionName + ":" + version + (buildUrl != null ? ":" + buildUrl : ""); } -} \ No newline at end of file +} diff --git a/server/embedded/src/org/labkey/embedded/LabKeyServer.java b/server/embedded/src/org/labkey/embedded/LabKeyServer.java index 1a319a8a09..8b1d8db23d 100644 --- a/server/embedded/src/org/labkey/embedded/LabKeyServer.java +++ b/server/embedded/src/org/labkey/embedded/LabKeyServer.java @@ -17,26 +17,46 @@ import org.apache.catalina.connector.Connector; import org.labkey.bootstrap.PipelineBootstrapConfig; +import org.labkey.bootstrap.StartupEnvironment; import org.springframework.boot.Banner; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.boot.context.ApplicationPidFileWriter; import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.boot.context.properties.bind.DefaultValue; import org.springframework.boot.tomcat.servlet.TomcatServletWebServerFactory; import org.springframework.boot.web.server.WebServerFactoryCustomizer; import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Configuration; -import org.springframework.validation.annotation.Validated; +import org.springframework.core.io.ClassPathResource; +import org.springframework.core.io.support.PropertiesLoaderUtils; import java.io.File; +import java.io.IOException; +import java.io.UncheckedIOException; import java.net.URLConnection; -import java.util.ArrayList; import java.util.Arrays; import java.util.HashMap; import java.util.List; import java.util.Map; +import java.util.Properties; +import java.util.Set; +import java.util.stream.Collectors; + +import static java.util.Objects.requireNonNullElse; @SpringBootApplication +@EnableConfigurationProperties({ + LabKeyServer.ContextProperties.class, + LabKeyServer.MailProperties.class, + LabKeyServer.GraphMailProperties.class, + LabKeyServer.CSPFilterProperties.class, + LabKeyServer.ServerSslProperties.class, + LabKeyServer.JsonAccessLog.class, + LabKeyServer.ManagementServerProperties.class, + LabKeyServer.LoggingProperties.class, + LabKeyServer.TomcatProperties.class +}) public class LabKeyServer { private static final String TERMINATE_ON_STARTUP_FAILURE = "terminateOnStartupFailure"; @@ -68,7 +88,9 @@ public static void main(String[] args) // Matches Tomcat's JreMemoryLeakPreventionListener (absent when embedded): cached jar: connections hold module jars open, blocking replacement on Windows URLConnection.setDefaultUseCaches("jar", false); - String logHome = PipelineBootstrapConfig.ensureLogHomeSet("logs"); + StartupEnvironment.ensureLabKeyHomeSet(new File("").getAbsoluteFile()); + // Resolves the labkey.log.home system property that labkey-defaults.properties reads + PipelineBootstrapConfig.ensureLogHomeSet("logs"); // Tomcat scans all jars matching the patterns below to find @ServerEndpoint and @WebListener annotations and // auto-register them. We restrict jar scanning to the absolute minimum to speed up server startup. Downside is @@ -84,153 +106,81 @@ public static void main(String[] args) SpringApplication application = new SpringApplication(LabKeyServer.class); application.addListeners(new ApplicationPidFileWriter("./labkey.pid")); - // A strong Content Security Policy - String baseCsp = """ - default-src 'self' ; - connect-src 'self' ${CONNECTION.SOURCES} ; - object-src ${OBJECT.SOURCES} ; /* Substitution value defaults to 'none' unless overridden by an admin */ - style-src 'self' 'unsafe-inline' ${STYLE.SOURCES} ; - img-src 'self' data: ${IMAGE.SOURCES} ; - font-src 'self' data: ${FONT.SOURCES} ; - script-src 'unsafe-eval' 'strict-dynamic' 'nonce-${REQUEST.SCRIPT.NONCE}' ${SCRIPT.SOURCES} ; - base-uri 'self' ; - frame-src 'self' ${FRAME.SOURCES} ; - report-uri ${context.contextPath:}/admin-contentSecurityPolicyReport.api ; - """; - // Add upgrade_insecure_requests substitution, frame-ancestors, and enforce version - String enforceCsp = baseCsp + """ - ${UPGRADE.INSECURE.REQUESTS} - frame-ancestors 'self' ${FRAMEANCESTORS.SOURCES} ; - /* cspVersion=e16 */ - """; - // Leave out upgrade_insecure_requests and frame-ancestors directives, since they produce warnings on some browsers - String reportCsp = baseCsp + """ - /* cspVersion=r16 */ - """; - - application.setDefaultProperties(new HashMap<>() - {{ - // GitHub Issue 796: JSON logging stopped after Tomcat/Spring update - // Propagate log4j configuration to Spring Boot config, which is necessary with Spring Boot 4.x - String log4JConfig = System.getProperty("log4j.configurationFile"); - if (log4JConfig != null) - { - String[] log4JConfigParts = log4JConfig.split(","); - if (log4JConfigParts.length > 0) - { - if ("log4j2.xml".equals(log4JConfigParts[0])) - { - // Assume this is the one packaged with our embedded build and on the classpath - put("logging.config", "classpath:log4j2.xml"); - } - else - { - put("logging.config", log4JConfigParts[0]); - } - if (log4JConfigParts.length > 1) - { - put("logging.log4j2.config.override", String.join(",", Arrays.asList(log4JConfigParts).subList(1, log4JConfigParts.length))); - } - } - } - - put("server.tomcat.basedir", "."); - put("server.tomcat.accesslog.directory", logHome); - - // Boost limits imposed by Tomcat v10.1.42 - put("server.tomcat.max-part-count", 500); - put("server.tomcat.max-part-header-size", 1024); // GitHub Issue 161: LKS insert forms can't handle long file field names - put("server.tomcat.max-connections", 1024); - // Boost limit back to Tomcat 10 level - put("server.tomcat.max-parameter-count", 10_000); - - // Enable HTTP compression for response content - put("server.compression.enabled", "true"); - // Spring Boot compresses HTML, JSON and other types by default, but not TSV, CSV, or SVG. - // We have to duplicate the defaults and add those types - put("server.compression.mime-types", "text/html,text/xml,text/plain,text/css,text/javascript,application/javascript,application/json,application/xml,text/tab-separated-values,text/csv,image/svg+xml"); - - put("server.tomcat.accesslog.enabled", "true"); - put("server.tomcat.accesslog.pattern", "%h %l %u %t \"%r\" %s %b %D %S %I \"%{Referer}i\" \"%{User-Agent}i\" %{LABKEY.username}s %{X-Forwarded-For}i"); - put("jsonaccesslog.pattern", "%h %t %m %U %s %b %D %S \"%{Referer}i\" \"%{User-Agent}i\" %{LABKEY.username}s %{X-Forwarded-For}i"); - - // Issue 52415: Omit stack traces from Tomcat error pages by default, but propagate error messages - put("server.error.include-stacktrace", "never"); - put("server.error.include-message", "always"); - - put("csp.enforce", enforceCsp); - put("csp.report", reportCsp); - - // GitHub Issue 692: Stop using CBC in HTTPS ciphers - // These settings configure HTTPS. Admins must opt in with additional settings - // in application.properties, like the key store. Without those other settings, - // HTTP-only startup fails unless "server.ssl.enabled" is explicitly set to false here - put("server.ssl.enabled", "false"); - put("server.ssl.protocol", "TLS"); - put("server.ssl.enabled-protocols", "TLSv1.3,TLSv1.2"); - // Use explicit JSSE cipher suite names to avoid CBC-mode suites - put("server.ssl.ciphers", - String.join(",", - // TLS 1.3 - "TLS_AES_256_GCM_SHA384", - "TLS_AES_128_GCM_SHA256", - "TLS_CHACHA20_POLY1305_SHA256", - // TLS 1.2 (AEAD only, no CBC) - "TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384", - "TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256", - "TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305_SHA256", - "TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384", - "TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256", - "TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305_SHA256" - ) - ); - put("server.ssl.use-cipher-suites-order", "true"); - - // GitHub Issue #1416 - default values for SMTP timeouts - put("mail.smtpConnectionTimeout", 10 * 1000); - put("mail.smtpTimeout", 60 * 1000); - // Unlike the socket-level timeouts above, JavaMail implements writetimeout with a ScheduledThreadPool per connection - one per message, since Transport.send() doesn't pool - put("mail.smtpWriteTimeout", 60 * 1000); - }} - ); + Map defaultProperties = loadBundledDefaults(); + defaultProperties.putAll(getLog4JProperties()); + application.setDefaultProperties(defaultProperties); application.setBannerMode(Banner.Mode.OFF); application.run(args); } - @Bean - public ContextProperties contextSource() + /** Not named application.properties, because spring.config.location would then drop it from the search path */ + private static Map loadBundledDefaults() { - return new ContextProperties(); + try + { + Properties properties = PropertiesLoaderUtils.loadProperties(new ClassPathResource("labkey-defaults.properties")); + Map result = new HashMap<>(); + properties.forEach((key, value) -> result.put((String) key, value)); + return result; + } + catch (IOException e) + { + throw new UncheckedIOException(e); + } } - @Bean - public MailProperties smtpSource() + /** + * GitHub Issue 796: JSON logging stopped after Tomcat/Spring update + * Propagate log4j configuration to Spring Boot config, which is necessary with Spring Boot 4.x. Can't live in + * labkey-defaults.properties because it's derived from a system property. + */ + private static Map getLog4JProperties() { - return new MailProperties(); - } + Map properties = new HashMap<>(); + String log4JConfig = System.getProperty("log4j.configurationFile"); - @Bean - public GraphMailProperties graphSource() - { - return new GraphMailProperties(); - } + if (log4JConfig != null) + { + String[] parts = log4JConfig.split(","); + if (parts.length > 0) + { + // "log4j2.xml" is the one packaged with our embedded build and on the classpath + properties.put("logging.config", "log4j2.xml".equals(parts[0]) ? "classpath:log4j2.xml" : parts[0]); + if (parts.length > 1) + { + properties.put("logging.log4j2.config.override", String.join(",", Arrays.asList(parts).subList(1, parts.length))); + } + } + } - @Bean - public CSPFilterProperties cspSource() - { - return new CSPFilterProperties(); + return properties; } - @Bean - public ServerSslProperties serverSslSource() + /** Record toString() that masks secret components, so logging a property holder can't leak credentials */ + private static String redactedToString(Record record, Set secrets) { - return new ServerSslProperties(); + return Arrays.stream(record.getClass().getRecordComponents()) + .map(component -> { + try + { + Object value = secrets.contains(component.getName()) ? "****" : component.getAccessor().invoke(record); + return component.getName() + "=" + value; + } + catch (ReflectiveOperationException e) + { + throw new RuntimeException(e); + } + }) + .collect(Collectors.joining(", ", record.getClass().getSimpleName() + "[", "]")); } @Bean - public JsonAccessLog jsonAccessLog() + public BootProperties bootProperties(ContextProperties context, MailProperties mail, GraphMailProperties graphMail, + CSPFilterProperties csp, ServerSslProperties serverSsl, JsonAccessLog jsonAccessLog, + ManagementServerProperties managementServer, LoggingProperties logging, + TomcatProperties tomcat) { - return new JsonAccessLog(); + return new BootProperties(context, mail, graphMail, csp, serverSsl, jsonAccessLog, managementServer, logging, tomcat); } @Bean @@ -241,801 +191,177 @@ public WebServerFactoryCustomizer customizer() } @Bean - public TomcatServletWebServerFactory servletContainerFactory() + public TomcatServletWebServerFactory servletContainerFactory(BootProperties properties) { - var result = new LabKeyTomcatServletWebServerFactory(this); + var result = new LabKeyTomcatServletWebServerFactory(properties); - var contextProperties = contextSource(); + Integer httpPort = properties.context().httpPort(); - if (contextProperties.getHttpPort() != null) + if (httpPort != null) { Connector httpConnector = new Connector(); httpConnector.setScheme("http"); - httpConnector.setPort(contextProperties.getHttpPort()); + httpConnector.setPort(httpPort); result.addAdditionalConnectors(httpConnector); } return result; } - @Configuration - @ConfigurationProperties("jsonaccesslog") - public static class JsonAccessLog - { - private boolean enabled; - private String pattern = "%h %t %m %U %s %b %D %S \"%{Referer}i\" \"%{User-Agent}i\" %{LABKEY.username}s"; - private String conditionIf; - private String conditionUnless; + /** Everything the boot layer binds from application.properties and hands to the webapp */ + public record BootProperties( + ContextProperties context, + MailProperties mail, + GraphMailProperties graphMail, + CSPFilterProperties csp, + ServerSslProperties serverSsl, + JsonAccessLog jsonAccessLog, + ManagementServerProperties managementServer, + LoggingProperties logging, + TomcatProperties tomcat + ) {} - public boolean isEnabled() - { - return enabled; - } - - public void setEnabled(boolean enabled) - { - this.enabled = enabled; - } - - public String getPattern() - { - return pattern; - } - - public void setPattern(String pattern) - { - this.pattern = pattern; - } - - public String getConditionIf() - { - return conditionIf; - } - - public void setConditionIf(String conditionIf) - { - this.conditionIf = conditionIf; - } - - public String getConditionUnless() - { - return conditionUnless; - } - - public void setConditionUnless(String conditionUnless) - { - this.conditionUnless = conditionUnless; - } - } - - @Bean - public ManagementServerProperties managementServerSource() - { - return new ManagementServerProperties(); - } - - @Bean - public LoggingProperties loggingSource() - { - return new LoggingProperties(); - } - - @Bean - public TomcatProperties tomcatProperties() - { - return new TomcatProperties(); - } + @ConfigurationProperties("jsonaccesslog") + public record JsonAccessLog( + boolean enabled, + String pattern, + String conditionIf, + String conditionUnless + ) {} /** * This lets us snoop on the Spring Boot config for deploying the management endpoint on a different port, as * we don't want to deploy LK on that port */ - @Configuration @ConfigurationProperties("management.server") - public static class ManagementServerProperties - { - private int _port; - - public int getPort() - { - return _port; - } - - public void setPort(int port) - { - _port = port; - } - } + public record ManagementServerProperties(int port) {} /** Values that we'll propagate to org.apache.catalina.filters.CorsFilter */ - public static class CorsProperties - { - private String _allowedOrigins; - private String _allowedMethods; - private String _allowedHeaders; - private String _exposedHeaders; - private String _supportCredentials; - private String _urlPattern; - private String _preflightMaxAge; - private String _requestDecorate; - - public String getAllowedOrigins() - { - return _allowedOrigins; - } - - public void setAllowedOrigins(String allowedOrigins) - { - _allowedOrigins = allowedOrigins; - } - - public String getAllowedMethods() - { - return _allowedMethods; - } - - public void setAllowedMethods(String allowedMethods) - { - _allowedMethods = allowedMethods; - } - - public String getAllowedHeaders() - { - return _allowedHeaders; - } - - public void setAllowedHeaders(String allowedHeaders) - { - _allowedHeaders = allowedHeaders; - } - - public String getExposedHeaders() - { - return _exposedHeaders; - } - - public void setExposedHeaders(String exposedHeaders) - { - _exposedHeaders = exposedHeaders; - } - - public String getSupportCredentials() - { - return _supportCredentials; - } - - public void setSupportCredentials(String supportCredentials) - { - _supportCredentials = supportCredentials; - } - - public String getUrlPattern() - { - return _urlPattern; - } - - public void setUrlPattern(String urlPattern) - { - _urlPattern = urlPattern; - } - - public String getPreflightMaxAge() - { - return _preflightMaxAge; - } - - public void setPreflightMaxAge(String preflightMaxAge) - { - _preflightMaxAge = preflightMaxAge; - } - - public String getRequestDecorate() - { - return _requestDecorate; - } - - public void setRequestDecorate(String requestDecorate) - { - _requestDecorate = requestDecorate; - } - } + public record CorsProperties( + String allowedOrigins, + String allowedMethods, + String allowedHeaders, + String exposedHeaders, + String supportCredentials, + String urlPattern, + String preflightMaxAge, + String requestDecorate + ) {} /** Add some properties that Spring Boot doesn't support setting. See issue 50690 */ - @Configuration @ConfigurationProperties("server.tomcat") - public static class TomcatProperties - { - private Boolean _useSendfile; - private Boolean _disableUploadTimeout; - private Boolean _useBodyEncodingForURI; - - private CorsProperties _cors; - - public Boolean getUseSendfile() - { - return _useSendfile; - } - - public void setUseSendfile(Boolean useSendfile) - { - _useSendfile = useSendfile; - } - - public Boolean getDisableUploadTimeout() - { - return _disableUploadTimeout; - } - - public void setDisableUploadTimeout(Boolean disableUploadTimeout) - { - _disableUploadTimeout = disableUploadTimeout; - } - - public Boolean getUseBodyEncodingForURI() - { - return _useBodyEncodingForURI; - } - - public void setUseBodyEncodingForURI(Boolean useBodyEncodingForURI) - { - _useBodyEncodingForURI = useBodyEncodingForURI; - } - - public CorsProperties getCors() - { - return _cors; - } - - public void setCors(CorsProperties cors) - { - _cors = cors; - } - } + public record TomcatProperties( + Boolean useSendfile, + Boolean disableUploadTimeout, + Boolean useBodyEncodingForURI, + CorsProperties cors + ) {} /** * This lets us snoop on the Spring Boot config for log4j so we can report it via a mothership metric */ - @Configuration @ConfigurationProperties("logging") - public static class LoggingProperties - { - private String _config; + public record LoggingProperties(String config) {} - public String getConfig() - { - return _config; - } - - public void setConfig(String config) - { - _config = config; - } - } - - - @Validated - @Configuration @ConfigurationProperties("context") - public static class ContextProperties + public record ContextProperties( + List dataSourceName, + List url, + List username, + List password, + List driverClassName, + String webAppLocation, + String workDirLocation, + String encryptionKey, + String oldEncryptionKey, + String legacyContextPath, + @DefaultValue("") String contextPath, + String pipelineConfig, + String requiredModules, + // Path to external modules directory + String externalModules, + boolean bypass2FA, + String serverGUID, + Integer httpPort, + Map maxTotal, + Map maxIdle, + Map maxWaitMillis, + Map accessToUnderlyingConnectionAllowed, + Map validationQuery, + Map displayName, + Map logQueries, + Map>> resources, + Map additionalWebapps + ) { - private List dataSourceName = new ArrayList<>(); - private List url = new ArrayList<>(); - private List username = new ArrayList<>(); - private List password = new ArrayList<>(); - private List driverClassName = new ArrayList<>(); - - private String webAppLocation; - private String workDirLocation; - private String encryptionKey; - private String oldEncryptionKey; - private String legacyContextPath; - - // Default to deploying to the root context path - private String contextPath = ""; - private String pipelineConfig; - private String requiredModules; - /** Path to external modules directory */ - private String externalModules; - private boolean bypass2FA = false; - private String serverGUID; - private Integer httpPort; - private Map maxTotal; - private Map maxIdle; - private Map maxWaitMillis; - private Map accessToUnderlyingConnectionAllowed; - private Map validationQuery; - private Map displayName; - private Map logQueries; - private Map>> resources; - private Map additionalWebapps; - - public List getDataSourceName() + public ContextProperties { - return dataSourceName; + dataSourceName = requireNonNullElse(dataSourceName, List.of()); + url = requireNonNullElse(url, List.of()); + username = requireNonNullElse(username, List.of()); + password = requireNonNullElse(password, List.of()); + driverClassName = requireNonNullElse(driverClassName, List.of()); } - public void setDataSourceName(List dataSourceName) - { - this.dataSourceName = dataSourceName; - } - - public List getUrl() - { - return url; - } - - public void setUrl(List url) - { - this.url = url; - } - - public List getUsername() - { - return username; - } - - public void setUsername(List username) - { - this.username = username; - } - - public List getPassword() - { - return password; - } - - public void setPassword(List password) - { - this.password = password; - } - - public List getDriverClassName() - { - return driverClassName; - } - - public void setDriverClassName(List driverClassName) - { - this.driverClassName = driverClassName; - } - - public String getWebAppLocation() - { - return webAppLocation; - } - - public void setWebAppLocation(String webAppLocation) - { - this.webAppLocation = webAppLocation; - } - - public String getWorkDirLocation() - { - return workDirLocation; - } - - public void setWorkDirLocation(String workDirLocation) - { - this.workDirLocation = workDirLocation; - } - - public String getEncryptionKey() + /** The server can't start without it, but nothing validates it until we're actually deploying the webapp */ + public String requireEncryptionKey() { if (null == encryptionKey) throw new RuntimeException("Must provide encryptionKey"); return encryptionKey; } - public void setEncryptionKey(String encryptionKey) - { - this.encryptionKey = encryptionKey; - } - - public String getOldEncryptionKey() + @Override + public String toString() { - return oldEncryptionKey; - } - - public void setOldEncryptionKey(String oldEncryptionKey) - { - this.oldEncryptionKey = oldEncryptionKey; - } - - public String getLegacyContextPath() - { - return legacyContextPath; - } - - public void setLegacyContextPath(String legacyContextPath) - { - this.legacyContextPath = legacyContextPath; - } - - public String getContextPath() - { - return contextPath; - } - - public void setContextPath(String contextPath) - { - this.contextPath = contextPath; - } - - public String getPipelineConfig() - { - return pipelineConfig; - } - - public void setPipelineConfig(String pipelineConfig) - { - this.pipelineConfig = pipelineConfig; - } - - public String getRequiredModules() - { - return requiredModules; - } - - public void setRequiredModules(String requiredModules) - { - this.requiredModules = requiredModules; - } - - public String getExternalModules() - { - return externalModules; - } - - public void setExternalModules(String externalModules) - { - this.externalModules = externalModules; - } - - public boolean isBypass2FA() - { - return bypass2FA; - } - - public void setBypass2FA(boolean bypass2FA) - { - this.bypass2FA = bypass2FA; - } - - public Integer getHttpPort() - { - return httpPort; - } - - public void setHttpPort(Integer httpPort) - { - this.httpPort = httpPort; - } - - public String getServerGUID() - { - return serverGUID; - } - - public void setServerGUID(String serverGUID) - { - this.serverGUID = serverGUID; - } - - public Map getMaxTotal() - { - return maxTotal; - } - - public void setMaxTotal(Map maxTotal) - { - this.maxTotal = maxTotal; - } - - - public void setMaxIdle(Map maxIdle) - { - this.maxIdle = maxIdle; - } - - public Map getMaxIdle() - { - return this.maxIdle; - } - - public void setAccessToUnderlyingConnectionAllowed(Map accessToUnderlyingConnectionAllowed) - { - this.accessToUnderlyingConnectionAllowed = accessToUnderlyingConnectionAllowed; - } - - public Map getAccessToUnderlyingConnectionAllowed() - { - return this.accessToUnderlyingConnectionAllowed; - } - - public void setMaxWaitMillis(Map maxWaitMillis) - { - this.maxWaitMillis = maxWaitMillis; - } - - public Map getMaxWaitMillis() - { - return this.maxWaitMillis; - } - - public Map getValidationQuery() - { - return validationQuery; - } - - public void setValidationQuery(Map validationQuery) - { - this.validationQuery = validationQuery; - } - - public Map getDisplayName() - { - return displayName; - } - - public void setDisplayName(Map displayName) - { - this.displayName = displayName; - } - - public Map getLogQueries() - { - return logQueries; - } - - public void setLogQueries(Map logQueries) - { - this.logQueries = logQueries; - } - - public Map>> getResources() - { - return resources; - } - - public void setResources(Map>> resources) - { - this.resources = resources; - } - - public Map getAdditionalWebapps() - { - return additionalWebapps; - } - - public void setAdditionalWebapps(Map additionalWebapps) - { - this.additionalWebapps = additionalWebapps; + return redactedToString(this, Set.of("password", "encryptionKey", "oldEncryptionKey")); } } - @Configuration @ConfigurationProperties("mail") - public static class MailProperties + public record MailProperties( + String smtpHost, + String smtpUser, + String smtpPort, + String smtpFrom, + String smtpPassword, + String smtpStartTlsEnable, + String smtpSocketFactoryClass, + String smtpAuth, + Integer smtpConnectionTimeout, + Integer smtpTimeout, + Integer smtpWriteTimeout + ) { - private String smtpHost; - private String smtpUser; - private String smtpPort; - private String smtpFrom; - private String smtpPassword; - private String smtpStartTlsEnable; - private String smtpSocketFactoryClass; - private String smtpAuth; - private Integer smtpConnectionTimeout; - private Integer smtpTimeout; - private Integer smtpWriteTimeout; - - public String getSmtpHost() + @Override + public String toString() { - return smtpHost; - } - - public void setSmtpHost(String smtpHost) - { - this.smtpHost = smtpHost; - } - - public String getSmtpUser() - { - return smtpUser; - } - - public void setSmtpUser(String smtpUser) - { - this.smtpUser = smtpUser; - } - - public String getSmtpPort() - { - return smtpPort; - } - - public void setSmtpPort(String smtpPort) - { - this.smtpPort = smtpPort; - } - - public String getSmtpFrom() - { - return smtpFrom; - } - - public void setSmtpFrom(String smtpFrom) - { - this.smtpFrom = smtpFrom; - } - - public String getSmtpPassword() - { - return smtpPassword; - } - - public void setSmtpPassword(String smtpPassword) - { - this.smtpPassword = smtpPassword; - } - - public String getSmtpStartTlsEnable() - { - return smtpStartTlsEnable; - } - - public void setSmtpStartTlsEnable(String smtpStartTlsEnable) - { - this.smtpStartTlsEnable = smtpStartTlsEnable; - } - - public String getSmtpSocketFactoryClass() - { - return smtpSocketFactoryClass; - } - - public void setSmtpSocketFactoryClass(String smtpSocketFactoryClass) - { - this.smtpSocketFactoryClass = smtpSocketFactoryClass; - } - - public String getSmtpAuth() - { - return smtpAuth; - } - - public void setSmtpAuth(String smtpAuth) - { - this.smtpAuth = smtpAuth; - } - - public Integer getSmtpConnectionTimeout() - { - return smtpConnectionTimeout; - } - - public void setSmtpConnectionTimeout(Integer smtpConnectionTimeout) - { - this.smtpConnectionTimeout = smtpConnectionTimeout; - } - - public Integer getSmtpTimeout() - { - return smtpTimeout; - } - - public void setSmtpTimeout(Integer smtpTimeout) - { - this.smtpTimeout = smtpTimeout; - } - - public Integer getSmtpWriteTimeout() - { - return smtpWriteTimeout; - } - - public void setSmtpWriteTimeout(Integer smtpWriteTimeout) - { - this.smtpWriteTimeout = smtpWriteTimeout; + return redactedToString(this, Set.of("smtpPassword")); } } - @Configuration @ConfigurationProperties("mail.graph") - public static class GraphMailProperties + public record GraphMailProperties( + String tenantId, + String clientId, + String clientSecret, + String fromAddress + ) { - private String tenantId; - private String clientId; - private String clientSecret; - private String fromAddress; - - public String getTenantId() + @Override + public String toString() { - return tenantId; - } - - public void setTenantId(String tenantId) - { - this.tenantId = tenantId; - } - - public String getClientId() - { - return clientId; - } - - public void setClientId(String clientId) - { - this.clientId = clientId; - } - - public String getClientSecret() - { - return clientSecret; - } - - public void setClientSecret(String clientSecret) - { - this.clientSecret = clientSecret; - } - - public String getFromAddress() - { - return fromAddress; - } - - public void setFromAddress(String fromAddress) - { - this.fromAddress = fromAddress; + return redactedToString(this, Set.of("clientSecret")); } } - @Configuration @ConfigurationProperties("csp") - public static class CSPFilterProperties - { - private String enforce; - private String report; - - public String getEnforce() - { - return enforce; - } - - public void setEnforce(String enforce) - { - this.enforce = enforce; - } - - public String getReport() - { - return report; - } - - public void setReport(String report) - { - this.report = report; - } - } + public record CSPFilterProperties(String enforce, String report) {} /** * Spring Boot doesn't propagate the keystore path into Tomcat's SSL config so we need to grab it and stash * it for potential use via the Connectors module. */ - @Configuration @ConfigurationProperties("server.ssl") - public static class ServerSslProperties - { - private String keyStore; - - public String getKeyStore() - { - return keyStore; - } - - public void setKeyStore(String keyStore) - { - this.keyStore = keyStore; - } - } + public record ServerSslProperties(String keyStore) {} } diff --git a/server/embedded/src/org/labkey/embedded/LabKeyTomcatServletWebServerFactory.java b/server/embedded/src/org/labkey/embedded/LabKeyTomcatServletWebServerFactory.java index e85abd874f..4f809d240f 100644 --- a/server/embedded/src/org/labkey/embedded/LabKeyTomcatServletWebServerFactory.java +++ b/server/embedded/src/org/labkey/embedded/LabKeyTomcatServletWebServerFactory.java @@ -29,7 +29,15 @@ import org.apache.logging.log4j.Logger; import org.apache.tomcat.util.descriptor.web.ContextResource; import org.labkey.bootstrap.ConfigException; +import org.labkey.bootstrap.StartupEnvironment; +import org.labkey.embedded.LabKeyServer.BootProperties; +import org.labkey.embedded.LabKeyServer.ContextProperties; +import org.labkey.embedded.LabKeyServer.CorsProperties; +import org.labkey.embedded.LabKeyServer.GraphMailProperties; +import org.labkey.embedded.LabKeyServer.JsonAccessLog; import org.labkey.embedded.LabKeyServer.MailProperties; +import org.labkey.embedded.LabKeyServer.ServerSslProperties; +import org.labkey.embedded.LabKeyServer.TomcatProperties; import org.springframework.boot.tomcat.TomcatWebServer; import org.springframework.boot.tomcat.servlet.TomcatServletWebServerFactory; import org.springframework.boot.web.servlet.ServletContextInitializer; @@ -49,29 +57,29 @@ class LabKeyTomcatServletWebServerFactory extends TomcatServletWebServerFactory private static final String CUSTOM_LOG4J_CONFIG = "org.labkey.customLog4JConfig"; private static final String CORS_PREFIX = "cors."; - private final LabKeyServer _server; + private final BootProperties _properties; - public LabKeyTomcatServletWebServerFactory(LabKeyServer server) + public LabKeyTomcatServletWebServerFactory(BootProperties properties) { - _server = server; + _properties = properties; addConnectorCustomizers(connector -> { - LabKeyServer.TomcatProperties props = _server.tomcatProperties(); + TomcatProperties props = _properties.tomcat(); - if (props.getUseBodyEncodingForURI() != null) + if (props.useBodyEncodingForURI() != null) { - connector.setUseBodyEncodingForURI(props.getUseBodyEncodingForURI()); + connector.setUseBodyEncodingForURI(props.useBodyEncodingForURI()); } if (connector.getProtocolHandler() instanceof AbstractHttp11Protocol handler) { - if (props.getDisableUploadTimeout() != null) + if (props.disableUploadTimeout() != null) { - handler.setDisableUploadTimeout(props.getDisableUploadTimeout()); + handler.setDisableUploadTimeout(props.disableUploadTimeout()); } - if (props.getUseSendfile() != null) + if (props.useSendfile() != null) { - handler.setUseSendfile(props.getUseSendfile()); + handler.setUseSendfile(props.useSendfile()); } } }); @@ -100,32 +108,29 @@ protected void prepareContext(Host host, ServletContextInitializer[] initializer @Override protected TomcatWebServer getTomcatWebServer(Tomcat tomcat) { - LabKeyServer.ManagementServerProperties props = _server.managementServerSource(); - // Don't deploy LK webapp on the separate instance running on the management port - if (props == null || props.getPort() != getPort()) + if (_properties.managementServer().port() != getPort()) { tomcat.enableNaming(); - // Get the context properties from Spring injection - LabKeyServer.ContextProperties contextProperties = _server.contextSource(); + ContextProperties contextProperties = _properties.context(); try { final File currentDir = new File("").getAbsoluteFile(); final File webAppLocation; - if (contextProperties.getWebAppLocation() == null) + if (contextProperties.webAppLocation() == null) { webAppLocation = new File(currentDir, "labkeywebapp"); } else { - webAppLocation = new File(contextProperties.getWebAppLocation()); + webAppLocation = new File(contextProperties.webAppLocation()); } EmbeddedExtractor extractor = new EmbeddedExtractor(false); - if (contextProperties.getWebAppLocation() == null || extractor.foundLabkeyServerJar()) + if (contextProperties.webAppLocation() == null || extractor.foundLabkeyServerJar()) { extractor.extractDistribution(webAppLocation); } // else, probably a local build deployment @@ -144,26 +149,18 @@ protected TomcatWebServer getTomcatWebServer(Tomcat tomcat) // call, but can immediately swap it with the desired place StandardContext context = (StandardContext) tomcat.addWebapp("/labkey", webAppLocation.getAbsolutePath()); // set the root path to the context explicitly - context.setPath(contextProperties.getContextPath()); + context.setPath(contextProperties.contextPath()); // Propagate standard Spring Boot properties such as the session timeout configureContext(context, Collections.emptyList()); - LabKeyServer.CSPFilterProperties cspFilterProperties = _server.cspSource(); - - if (cspFilterProperties.getEnforce() != null) - { - context.addParameter("csp.enforce", cspFilterProperties.getEnforce()); - } - if (cspFilterProperties.getReport() != null) - { - context.addParameter("csp.report", cspFilterProperties.getReport()); - } + addContextProperty(context, _properties.csp().enforce(), "csp.enforce"); + addContextProperty(context, _properties.csp().report(), "csp.report"); // Issue 48426: Allow config for desired work directory - if (contextProperties.getWorkDirLocation() != null) + if (contextProperties.workDirLocation() != null) { - context.setWorkDir(contextProperties.getWorkDirLocation()); + context.setWorkDir(contextProperties.workDirLocation()); } // Push the JDBC connection for the primary DB into the context so that the LabKey webapp finds them @@ -177,71 +174,55 @@ protected TomcatWebServer getTomcatWebServer(Tomcat tomcat) addGraphProperties(context); // Add the encryption key(s) - context.addParameter("EncryptionKey", contextProperties.getEncryptionKey()); - if (contextProperties.getOldEncryptionKey() != null) - { - context.addParameter("OldEncryptionKey", contextProperties.getOldEncryptionKey()); - } + context.addParameter("EncryptionKey", contextProperties.requireEncryptionKey()); + addContextProperty(context, contextProperties.oldEncryptionKey(), "OldEncryptionKey"); - if (contextProperties.getLegacyContextPath() != null) + if (contextProperties.legacyContextPath() != null) { - if (contextProperties.getContextPath() != null && !contextProperties.getContextPath().isEmpty() && !contextProperties.getContextPath().equals("/")) + if (contextProperties.contextPath() != null && !contextProperties.contextPath().isEmpty() && !contextProperties.contextPath().equals("/")) { throw new ConfigException("contextPath.legacyContextPath is only intended for use when deploying the LabKey application to the root context path. Please update application.properties."); } - context.addParameter("legacyContextPath", contextProperties.getLegacyContextPath()); + context.addParameter("legacyContextPath", contextProperties.legacyContextPath()); } - if (contextProperties.getRequiredModules() != null) - { - context.addParameter("requiredModules", contextProperties.getRequiredModules()); - } - if (contextProperties.getExternalModules() != null) + addContextProperty(context, contextProperties.requiredModules(), "requiredModules"); + if (contextProperties.externalModules() != null) { // We've long supported configuring this via a system property so propagate the value - System.setProperty("labkey.externalModulesDir", contextProperties.getExternalModules()); - } - if (contextProperties.getPipelineConfig() != null) - { - context.addParameter("org.labkey.api.pipeline.config", contextProperties.getPipelineConfig()); + System.setProperty("labkey.externalModulesDir", contextProperties.externalModules()); } - if (contextProperties.isBypass2FA()) + addContextProperty(context, contextProperties.pipelineConfig(), "org.labkey.api.pipeline.config"); + if (contextProperties.bypass2FA()) { - // Expand single config into two different options. Can collapse/rename when we're embedded-only, - // but this provides an easy backwards compatible bridge while we still support standalone Tomcat - context.addParameter("org.labkey.authentication.totp.Bypass", "true"); - context.addParameter("org.labkey.authentication.duo.Bypass", "true"); + context.addParameter(StartupEnvironment.BYPASS_2FA_PARAMETER, "true"); } boolean customLog4J = System.getProperty("log4j.configurationFile") != null; if (!customLog4J) { - customLog4J = _server.loggingSource().getConfig() != null; + customLog4J = _properties.logging().config() != null; } context.addParameter(CUSTOM_LOG4J_CONFIG, Boolean.toString(customLog4J)); // Add serverGUID for mothership - it tells mothership that 2 instances of a server should be considered the same for metrics gathering purposes - addContextProperty(context, contextProperties.getServerGUID(), SERVER_GUID_PARAMETER_NAME); + addContextProperty(context, contextProperties.serverGUID(), SERVER_GUID_PARAMETER_NAME); - LabKeyServer.TomcatProperties tomcatProperties = _server.tomcatProperties(); - if (tomcatProperties != null && tomcatProperties.getCors() != null) + CorsProperties corsProperties = _properties.tomcat().cors(); + if (corsProperties != null) { // Push these into the context so that ApiModule can register Tomcat's CorsFilter as desired - LabKeyServer.CorsProperties corsProperties = tomcatProperties.getCors(); - addContextProperty(context, corsProperties.getAllowedOrigins(), CORS_PREFIX + "allowedOrigins"); - addContextProperty(context, corsProperties.getAllowedMethods(), CORS_PREFIX + "allowedMethods"); - addContextProperty(context, corsProperties.getAllowedHeaders(), CORS_PREFIX + "allowedHeaders"); - addContextProperty(context, corsProperties.getExposedHeaders(), CORS_PREFIX + "exposedHeaders"); - addContextProperty(context, corsProperties.getSupportCredentials(), CORS_PREFIX + "supportCredentials"); - addContextProperty(context, corsProperties.getUrlPattern(), CORS_PREFIX + "urlPattern"); - addContextProperty(context, corsProperties.getPreflightMaxAge(), CORS_PREFIX + "preflightMaxAge"); - addContextProperty(context, corsProperties.getRequestDecorate(), CORS_PREFIX + "requestDecorate"); + addContextProperty(context, corsProperties.allowedOrigins(), CORS_PREFIX + "allowedOrigins"); + addContextProperty(context, corsProperties.allowedMethods(), CORS_PREFIX + "allowedMethods"); + addContextProperty(context, corsProperties.allowedHeaders(), CORS_PREFIX + "allowedHeaders"); + addContextProperty(context, corsProperties.exposedHeaders(), CORS_PREFIX + "exposedHeaders"); + addContextProperty(context, corsProperties.supportCredentials(), CORS_PREFIX + "supportCredentials"); + addContextProperty(context, corsProperties.urlPattern(), CORS_PREFIX + "urlPattern"); + addContextProperty(context, corsProperties.preflightMaxAge(), CORS_PREFIX + "preflightMaxAge"); + addContextProperty(context, corsProperties.requestDecorate(), CORS_PREFIX + "requestDecorate"); } - LabKeyServer.ServerSslProperties sslProps = _server.serverSslSource(); - if (null != sslProps && sslProps.getKeyStore() != null) - { - context.addParameter(SERVER_SSL_KEYSTORE, sslProps.getKeyStore()); - } + ServerSslProperties sslProps = _properties.serverSsl(); + addContextProperty(context, sslProps.keyStore(), SERVER_SSL_KEYSTORE); // Point at the special classloader with the hack for SLF4J WebappLoader loader = new WebappLoader(); @@ -254,13 +235,13 @@ protected TomcatWebServer getTomcatWebServer(Tomcat tomcat) throw new RuntimeException(e); } - LabKeyServer.JsonAccessLog logConfig = _server.jsonAccessLog(); - if (logConfig.isEnabled()) + JsonAccessLog logConfig = _properties.jsonAccessLog(); + if (logConfig.enabled()) { configureJsonAccessLogging(tomcat, logConfig); } - Map additionalWebapps = contextProperties.getAdditionalWebapps(); + Map additionalWebapps = contextProperties.additionalWebapps(); if (additionalWebapps != null) { setRegisterDefaultServlet(true); @@ -310,7 +291,7 @@ private void addContextProperty(StandardContext context, String value, String na } // Issue 48565: allow for JSON-formatted access logs - private void configureJsonAccessLogging(Tomcat tomcat, LabKeyServer.JsonAccessLog logConfig) + private void configureJsonAccessLogging(Tomcat tomcat, JsonAccessLog logConfig) { var v = new JsonAccessLogValve(); @@ -323,9 +304,9 @@ private void configureJsonAccessLogging(Tomcat tomcat, LabKeyServer.JsonAccessLo v.setContainer(tomcat.getHost()); // Now the settings that we support via application.properties - v.setPattern(logConfig.getPattern()); - v.setConditionIf(logConfig.getConditionIf()); - v.setConditionUnless(logConfig.getConditionUnless()); + v.setPattern(logConfig.pattern()); + v.setConditionIf(logConfig.conditionIf()); + v.setConditionUnless(logConfig.conditionUnless()); tomcat.getEngine().getPipeline().addValve(v); } @@ -335,14 +316,14 @@ private void configureJsonAccessLogging(Tomcat tomcat, LabKeyServer.JsonAccessLo * context.dataSourceName[0]=jdbc/labkeyDataSource * context.driverClassName[0]=org.postgresql.Driver */ - private void addDataSourceResources(LabKeyServer.ContextProperties props, StandardContext context) throws ConfigException + private void addDataSourceResources(ContextProperties props, StandardContext context) throws ConfigException { - var numOfDataResources = props.getUrl().size(); + var numOfDataResources = props.url().size(); - if (numOfDataResources != props.getDataSourceName().size() || - numOfDataResources != props.getDriverClassName().size() || - numOfDataResources != props.getUsername().size() || - numOfDataResources != props.getPassword().size()) + if (numOfDataResources != props.dataSourceName().size() || + numOfDataResources != props.driverClassName().size() || + numOfDataResources != props.username().size() || + numOfDataResources != props.password().size()) { throw new ConfigException("DataSources not configured properly. Must have all the required properties for all datasources: dataSourceName, driverClassName, userName, password, url"); } @@ -350,27 +331,27 @@ private void addDataSourceResources(LabKeyServer.ContextProperties props, Standa for (int i = 0; i < numOfDataResources; i++) { ContextResource dataSourceResource = new ContextResource(); - dataSourceResource.setName(props.getDataSourceName().get(i)); + dataSourceResource.setName(props.dataSourceName().get(i)); dataSourceResource.setAuth("Container"); dataSourceResource.setType(DataSource.class.getName()); - dataSourceResource.setProperty("driverClassName", props.getDriverClassName().get(i)); - dataSourceResource.setProperty("url", props.getUrl().get(i)); - dataSourceResource.setProperty("password", props.getPassword().get(i)); - dataSourceResource.setProperty("username", props.getUsername().get(i)); + dataSourceResource.setProperty("driverClassName", props.driverClassName().get(i)); + dataSourceResource.setProperty("url", props.url().get(i)); + dataSourceResource.setProperty("password", props.password().get(i)); + dataSourceResource.setProperty("username", props.username().get(i)); - dataSourceResource.setProperty("maxTotal", getPropValue(props.getMaxTotal(), i, LabKeyServer.MAX_TOTAL_CONNECTIONS_DEFAULT, "maxTotal")); - dataSourceResource.setProperty("maxIdle", getPropValue(props.getMaxIdle(), i, LabKeyServer.MAX_IDLE_DEFAULT, "maxIdle")); - dataSourceResource.setProperty("maxWaitMillis", getPropValue(props.getMaxWaitMillis(), i, LabKeyServer.MAX_WAIT_MILLIS_DEFAULT, "maxWaitMillis")); - dataSourceResource.setProperty("accessToUnderlyingConnectionAllowed", getPropValue(props.getAccessToUnderlyingConnectionAllowed(), i, LabKeyServer.ACCESS_TO_CONNECTION_ALLOWED_DEFAULT, "accessToUnderlyingConnectionAllowed")); - dataSourceResource.setProperty("validationQuery", getPropValue(props.getValidationQuery(), i, LabKeyServer.VALIDATION_QUERY_DEFAULT, "validationQuery")); + dataSourceResource.setProperty("maxTotal", getPropValue(props.maxTotal(), i, LabKeyServer.MAX_TOTAL_CONNECTIONS_DEFAULT, "maxTotal")); + dataSourceResource.setProperty("maxIdle", getPropValue(props.maxIdle(), i, LabKeyServer.MAX_IDLE_DEFAULT, "maxIdle")); + dataSourceResource.setProperty("maxWaitMillis", getPropValue(props.maxWaitMillis(), i, LabKeyServer.MAX_WAIT_MILLIS_DEFAULT, "maxWaitMillis")); + dataSourceResource.setProperty("accessToUnderlyingConnectionAllowed", getPropValue(props.accessToUnderlyingConnectionAllowed(), i, LabKeyServer.ACCESS_TO_CONNECTION_ALLOWED_DEFAULT, "accessToUnderlyingConnectionAllowed")); + dataSourceResource.setProperty("validationQuery", getPropValue(props.validationQuery(), i, LabKeyServer.VALIDATION_QUERY_DEFAULT, "validationQuery")); // These two properties are handled differently, as separate parameters - String displayName = getPropValue(props.getDisplayName(), i, null, "displayName"); + String displayName = getPropValue(props.displayName(), i, null, "displayName"); if (displayName != null) { context.addParameter(dataSourceResource.getName() + ":DisplayName", displayName); } - String logQueries = getPropValue(props.getLogQueries(), i, null, "logQueries"); + String logQueries = getPropValue(props.logQueries(), i, null, "logQueries"); if (logQueries != null) { context.addParameter(dataSourceResource.getName() + ":LogQueries", logQueries); @@ -380,9 +361,9 @@ private void addDataSourceResources(LabKeyServer.ContextProperties props, Standa } } - private void addExtraContextResources(LabKeyServer.ContextProperties contextProperties, StandardContext context) throws ConfigException + private void addExtraContextResources(ContextProperties contextProperties, StandardContext context) throws ConfigException { - Map>> resourceMaps = Objects.requireNonNullElse(contextProperties.getResources(), Collections.emptyMap()); + Map>> resourceMaps = Objects.requireNonNullElse(contextProperties.resources(), Collections.emptyMap()); for (Map.Entry>> parentEntry : resourceMaps.entrySet()) { @@ -425,19 +406,19 @@ private String getPropValue(Map propValues, Integer resourceKey private void addSmtpProperties(StandardContext context) { // Get session/mail properties - MailProperties mailProps = _server.smtpSource(); - - addSmtpProperty(context, "host", mailProps.getSmtpHost()); - addSmtpProperty(context, "user", mailProps.getSmtpUser()); - addSmtpProperty(context, "port", mailProps.getSmtpPort()); - addSmtpProperty(context, "from", mailProps.getSmtpFrom()); - addSmtpProperty(context, "password", mailProps.getSmtpPassword()); - addSmtpProperty(context, "starttls.enable", mailProps.getSmtpStartTlsEnable()); - addSmtpProperty(context, "socketFactory.class", mailProps.getSmtpSocketFactoryClass()); - addSmtpProperty(context, "auth", mailProps.getSmtpAuth()); - addSmtpProperty(context, "connectiontimeout", mailProps.getSmtpConnectionTimeout()); - addSmtpProperty(context, "timeout", mailProps.getSmtpTimeout()); - addSmtpProperty(context, "writetimeout", mailProps.getSmtpWriteTimeout()); + MailProperties mailProps = _properties.mail(); + + addSmtpProperty(context, "host", mailProps.smtpHost()); + addSmtpProperty(context, "user", mailProps.smtpUser()); + addSmtpProperty(context, "port", mailProps.smtpPort()); + addSmtpProperty(context, "from", mailProps.smtpFrom()); + addSmtpProperty(context, "password", mailProps.smtpPassword()); + addSmtpProperty(context, "starttls.enable", mailProps.smtpStartTlsEnable()); + addSmtpProperty(context, "socketFactory.class", mailProps.smtpSocketFactoryClass()); + addSmtpProperty(context, "auth", mailProps.smtpAuth()); + addSmtpProperty(context, "connectiontimeout", mailProps.smtpConnectionTimeout()); + addSmtpProperty(context, "timeout", mailProps.smtpTimeout()); + addSmtpProperty(context, "writetimeout", mailProps.smtpWriteTimeout()); } private void addSmtpProperty(StandardContext context, String name, String value) @@ -457,23 +438,11 @@ private void addSmtpProperty(StandardContext context, String name, Integer value private void addGraphProperties(StandardContext context) { // Get Microsoft Graph mail properties - LabKeyServer.GraphMailProperties graphProps = _server.graphSource(); + GraphMailProperties graphProps = _properties.graphMail(); - if (graphProps.getTenantId() != null) - { - context.addParameter("mail.graph.tenantId", graphProps.getTenantId()); - } - if (graphProps.getClientId() != null) - { - context.addParameter("mail.graph.clientId", graphProps.getClientId()); - } - if (graphProps.getClientSecret() != null) - { - context.addParameter("mail.graph.clientSecret", graphProps.getClientSecret()); - } - if (graphProps.getFromAddress() != null) - { - context.addParameter("mail.graph.fromAddress", graphProps.getFromAddress()); - } + addContextProperty(context, graphProps.tenantId(), "mail.graph.tenantId"); + addContextProperty(context, graphProps.clientId(), "mail.graph.clientId"); + addContextProperty(context, graphProps.clientSecret(), "mail.graph.clientSecret"); + addContextProperty(context, graphProps.fromAddress(), "mail.graph.fromAddress"); } }