Skip to content

Commit 9e8b2d4

Browse files
committed
Add validation framework for imposing specific rules for configuration
values and a single PortValidator as a POC, and add a LifecycleListener that is capable of stopping the startup process when there are validation failures. This is a proof-of-concept implementation that enhances the configtest command with validation capabilities, focusing on port configuration as a valuable starting point from dev-list discussion. The configtest behavior hasn't changed unless you use the --validate-only option to produce validation output instead of the typical server startup attempt. There's also a new command in catalina for config-validate for ease of use. Port validation detects: - Port conflicts (already in use) - Invalid port numbers (< 0 or > 65535) - Duplicate port assignments across connectors - Privileged ports (< 1024) without root access - Default/insecure shutdown commands - AJP connectors missing required 'secret' attribute - AJP connectors listening on all interfaces (0.0.0.0)
1 parent fbb6f7b commit 9e8b2d4

File tree

12 files changed

+1173
-21
lines changed

12 files changed

+1173
-21
lines changed

bin/catalina.bat

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -263,6 +263,7 @@ if ""%1"" == ""run"" goto doRun
263263
if ""%1"" == ""start"" goto doStart
264264
if ""%1"" == ""stop"" goto doStop
265265
if ""%1"" == ""configtest"" goto doConfigTest
266+
if ""%1"" == ""config-validate"" goto doConfigValidate
266267
if ""%1"" == ""version"" goto doVersion
267268

268269
echo Usage: catalina ( commands ... )
@@ -273,6 +274,7 @@ echo run Start Catalina in the current window
273274
echo start Start Catalina in a separate window
274275
echo stop Stop Catalina
275276
echo configtest Run a basic syntax check on server.xml
277+
echo config-validate Run configuration validators with detailed output
276278
echo version What version of tomcat are you running?
277279
goto end
278280

@@ -300,7 +302,19 @@ goto execCmd
300302

301303
:doConfigTest
302304
shift
303-
set ACTION=configtest
305+
rem Check if --validate-only argument is present
306+
if ""%1"" == ""--validate-only"" (
307+
set ACTION=config-validate
308+
shift
309+
) else (
310+
set ACTION=configtest
311+
)
312+
set CATALINA_OPTS=
313+
goto execCmd
314+
315+
:doConfigValidate
316+
shift
317+
set ACTION=config-validate
304318
set CATALINA_OPTS=
305319
goto execCmd
306320

bin/catalina.sh

Lines changed: 19 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -549,18 +549,35 @@ elif [ "$1" = "stop" ] ; then
549549

550550
elif [ "$1" = "configtest" ] ; then
551551

552+
# Check if --validate-only argument is present
553+
if [ "$2" = "--validate-only" ] ; then
554+
COMMAND="config-validate"
555+
else
556+
COMMAND="configtest"
557+
fi
558+
552559
eval "\"$_RUNJAVA\"" $LOGGING_MANAGER "$JAVA_OPTS" \
553560
-classpath "\"$CLASSPATH\"" \
554561
-Dcatalina.base="\"$CATALINA_BASE\"" \
555562
-Dcatalina.home="\"$CATALINA_HOME\"" \
556563
-Djava.io.tmpdir="\"$CATALINA_TMPDIR\"" \
557-
org.apache.catalina.startup.Bootstrap configtest
564+
org.apache.catalina.startup.Bootstrap "$COMMAND"
558565
result=$?
559-
if [ $result -ne 0 ]; then
566+
if [ $result -ne 0 ] && [ "$COMMAND" = "configtest" ]; then
560567
echo "Configuration error detected!"
561568
fi
562569
exit $result
563570

571+
elif [ "$1" = "config-validate" ] ; then
572+
573+
eval "\"$_RUNJAVA\"" $LOGGING_MANAGER "$JAVA_OPTS" \
574+
-classpath "\"$CLASSPATH\"" \
575+
-Dcatalina.base="\"$CATALINA_BASE\"" \
576+
-Dcatalina.home="\"$CATALINA_HOME\"" \
577+
-Djava.io.tmpdir="\"$CATALINA_TMPDIR\"" \
578+
org.apache.catalina.startup.Bootstrap config-validate
579+
exit $?
580+
564581
elif [ "$1" = "version" ] ; then
565582

566583
eval "\"$_RUNJAVA\"" "$JAVA_OPTS" \

java/org/apache/catalina/startup/Bootstrap.java

Lines changed: 38 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -275,9 +275,25 @@ public void init() throws Exception {
275275
* Load daemon.
276276
*/
277277
private void load(String[] arguments) throws Exception {
278+
invokeCatalinaMethod("load", arguments);
279+
}
280+
281+
/**
282+
* Load configuration only without initializing server.
283+
* Used for validation without binding to ports.
284+
*/
285+
private void loadConfigOnly(String[] arguments) throws Exception {
286+
invokeCatalinaMethod("loadConfigOnly", arguments);
287+
}
278288

279-
// Call the load() method
280-
String methodName = "load";
289+
/**
290+
* Helper method to invoke a Catalina method via reflection with optional arguments.
291+
*
292+
* @param methodName the name of the method to invoke
293+
* @param arguments optional arguments to pass to the method
294+
* @throws Exception if the method invocation fails
295+
*/
296+
private void invokeCatalinaMethod(String methodName, String[] arguments) throws Exception {
281297
Object[] param;
282298
Class<?>[] paramTypes;
283299
if (arguments == null || arguments.length == 0) {
@@ -296,7 +312,6 @@ private void load(String[] arguments) throws Exception {
296312
method.invoke(catalinaDaemon, param);
297313
}
298314

299-
300315
/**
301316
* getServer() for configtest
302317
*/
@@ -307,6 +322,18 @@ private Object getServer() throws Exception {
307322
return method.invoke(catalinaDaemon);
308323
}
309324

325+
/**
326+
* Run configuration validation tests.
327+
*
328+
* @return exit code (0 = success, 1 = errors found)
329+
* @throws Exception Fatal validation error
330+
*/
331+
public int configtest() throws Exception {
332+
Method method = catalinaDaemon.getClass().getMethod("configtest");
333+
Integer exitCode = (Integer) method.invoke(catalinaDaemon);
334+
return exitCode != null ? exitCode.intValue() : 1;
335+
}
336+
310337

311338
// ----------------------------------------------------------- Main Program
312339

@@ -482,6 +509,14 @@ public static void main(String[] args) {
482509
}
483510
System.exit(0);
484511
break;
512+
case "config-validate":
513+
daemon.loadConfigOnly(args);
514+
if (null == daemon.getServer()) {
515+
System.exit(1);
516+
}
517+
int exitCode = daemon.configtest();
518+
System.exit(exitCode);
519+
break;
485520
default:
486521
log.warn("Bootstrap: command \"" + command + "\" does not exist.");
487522
break;

java/org/apache/catalina/startup/Catalina.java

Lines changed: 114 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -343,6 +343,9 @@ protected boolean arguments(String[] args) {
343343
} else if (arg.equals("configtest")) {
344344
isGenerateCode = false;
345345
// NOOP
346+
} else if (arg.equals("config-validate")) {
347+
isGenerateCode = false;
348+
// NOOP
346349
} else if (arg.equals("stop")) {
347350
isGenerateCode = false;
348351
// NOOP
@@ -673,17 +676,34 @@ public void stopServer(String[] arguments) {
673676
}
674677

675678

679+
/**
680+
* Load configuration without initializing the server.
681+
* Used for configuration validation without binding to ports.
682+
*/
683+
public void loadConfigOnly() {
684+
loadInternal(false);
685+
}
686+
676687
/**
677688
* Start a new server instance.
678689
*/
679690
public void load() {
691+
loadInternal(true);
692+
}
680693

694+
/**
695+
* Internal load method that handles both config-only and full initialization.
696+
*
697+
* @param initServer if true, initialize the server and bind to ports;
698+
* if false, only parse configuration
699+
*/
700+
private void loadInternal(boolean initServer) {
681701
if (loaded) {
682702
return;
683703
}
684704
loaded = true;
685705

686-
long t1 = System.nanoTime();
706+
long t1 = initServer ? System.nanoTime() : 0;
687707

688708
// Before digester - it may be needed
689709
initNaming();
@@ -699,23 +719,25 @@ public void load() {
699719
getServer().setCatalinaHome(Bootstrap.getCatalinaHomeFile());
700720
getServer().setCatalinaBase(Bootstrap.getCatalinaBaseFile());
701721

702-
// Stream redirection
703-
initStreams();
722+
if (initServer) {
723+
// Stream redirection
724+
initStreams();
704725

705-
// Start the new server
706-
try {
707-
getServer().init();
708-
} catch (LifecycleException e) {
709-
if (throwOnInitFailure) {
710-
throw new Error(e);
711-
} else {
712-
log.error(sm.getString("catalina.initError"), e);
726+
// Start the new server
727+
try {
728+
getServer().init();
729+
} catch (LifecycleException e) {
730+
if (throwOnInitFailure) {
731+
throw new Error(e);
732+
} else {
733+
log.error(sm.getString("catalina.initError"), e);
734+
}
713735
}
714-
}
715736

716-
if (log.isInfoEnabled()) {
717-
log.info(sm.getString("catalina.init",
718-
Long.toString(TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - t1))));
737+
if (log.isInfoEnabled()) {
738+
log.info(sm.getString("catalina.init",
739+
Long.toString(TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - t1))));
740+
}
719741
}
720742
}
721743

@@ -734,6 +756,20 @@ public void load(String[] args) {
734756
}
735757
}
736758

759+
/*
760+
* Load configuration only using arguments
761+
*/
762+
public void loadConfigOnly(String[] args) {
763+
764+
try {
765+
if (arguments(args)) {
766+
loadConfigOnly();
767+
}
768+
} catch (Exception e) {
769+
e.printStackTrace(System.out);
770+
}
771+
}
772+
737773

738774
/**
739775
* Start a new server instance.
@@ -857,6 +893,69 @@ protected void usage() {
857893

858894
}
859895

896+
/**
897+
* Run configuration validation tests.
898+
*
899+
* @return exit code (0 = success, 1 = errors found)
900+
*/
901+
public int configtest() {
902+
if (server == null) {
903+
return 1;
904+
}
905+
906+
try {
907+
908+
// Run validators
909+
org.apache.catalina.startup.validator.ValidatorRegistry registry =
910+
new org.apache.catalina.startup.validator.ValidatorRegistry();
911+
org.apache.catalina.startup.validator.ValidationResult result =
912+
registry.validate(server);
913+
914+
// Print results
915+
System.out.println();
916+
System.out.println("Configuration Validation Results");
917+
System.out.println("=================================");
918+
System.out.println();
919+
920+
if (!result.hasFindings()) {
921+
System.out.println("No issues found. Configuration is valid.");
922+
return 0;
923+
}
924+
925+
// Group findings by severity
926+
java.util.List<org.apache.catalina.startup.validator.ValidationResult.Finding> findings =
927+
result.getFindings();
928+
929+
for (org.apache.catalina.startup.validator.ValidationResult.Finding finding : findings) {
930+
System.out.println(finding.toString());
931+
}
932+
933+
System.out.println();
934+
System.out.println("Summary: " + result.getErrorCount() + " error(s), " +
935+
result.getWarningCount() + " warning(s), " +
936+
result.getInfoCount() + " info message(s)");
937+
System.out.println();
938+
939+
if (result.getErrorCount() > 0) {
940+
System.out.println("Configuration test FAILED.");
941+
System.out.println("Configuration error detected!");
942+
return 1;
943+
} else {
944+
if (result.getWarningCount() > 0) {
945+
System.out.println("Configuration test PASSED (with warnings).");
946+
} else {
947+
System.out.println("Configuration test PASSED.");
948+
}
949+
return 0;
950+
}
951+
952+
} catch (Exception e) {
953+
log.error(sm.getString("catalina.configTestError"), e);
954+
e.printStackTrace();
955+
return 1;
956+
}
957+
}
958+
860959

861960
protected void initStreams() {
862961
// Replace System.out and System.err with a custom PrintStream
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
# Licensed to the Apache Software Foundation (ASF) under one or more
2+
# contributor license agreements. See the NOTICE file distributed with
3+
# this work for additional information regarding copyright ownership.
4+
# The ASF licenses this file to You under the Apache License, Version 2.0
5+
# (the "License"); you may not use this file except in compliance with
6+
# the License. You may obtain a copy of the License at
7+
#
8+
# http://www.apache.org/licenses/LICENSE-2.0
9+
#
10+
# Unless required by applicable law or agreed to in writing, software
11+
# distributed under the License is distributed on an "AS IS" BASIS,
12+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
# See the License for the specific language governing permissions and
14+
# limitations under the License.
15+
16+
# StartupValidationListener messages
17+
startupValidationListener.notServer=StartupValidationListener can only be attached to a Server
18+
startupValidationListener.starting=Running configuration validation checks...
19+
startupValidationListener.abortingOnErrors=Aborting startup due to {0} configuration errors
20+
startupValidationListener.complete=Configuration validation complete: {0} errors, {1} warnings, {2} info messages
21+
22+
# PortValidator messages
23+
portValidator.shutdownDisabled=Shutdown port is disabled (port < 0)
24+
portValidator.shutdownCommandDefault=Shutdown command is set to default value "SHUTDOWN" on port {0}. Consider using a custom shutdown command.
25+
portValidator.invalidPort=Invalid port number: {0}
26+
portValidator.privilegedPort=Port {0} requires root/administrator privileges (current user: {1})
27+
portValidator.portInUse=Port {0} is already in use
28+
portValidator.shutdownPortInUse=Shutdown port {0} is already in use
29+
portValidator.duplicatePort=Port {0} is already configured for: {1}
30+
portValidator.invalidAddress=Invalid bind address: {0}
31+
portValidator.ajpMissingSecret=AJP connector is missing required ''secret'' attribute for secure operation
32+
portValidator.ajpListeningAll=AJP connector is listening on all interfaces (0.0.0.0). Consider binding to localhost or specific IP.

0 commit comments

Comments
 (0)