Skip to content
Original file line number Diff line number Diff line change
Expand Up @@ -47,14 +47,9 @@ public class LabKeyBootstrapClassLoader extends WebappClassLoader implements Exp
private final Set<String> _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;
Expand Down
58 changes: 58 additions & 0 deletions server/bootstrap/src/org/labkey/bootstrap/StartupEnvironment.java
Original file line number Diff line number Diff line change
@@ -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);
}
}
9 changes: 3 additions & 6 deletions server/embedded/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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}"
Expand Down
70 changes: 70 additions & 0 deletions server/embedded/src/main/resources/labkey-defaults.properties
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -451,4 +453,4 @@ public String toString()
{
return distributionName + ":" + version + (buildUrl != null ? ":" + buildUrl : "");
}
}
}
Loading
Loading