1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 513 514 515 516 517 518 519 520 521 522 523 524 525 526 527 528 529 530 531 532 533 534 535 536 537 538 539 540 541 542 543 544 545 546 547 548 549 550 551 552 553 554 555 556 557 558 559 560 561 562 563 564 565 566 567 568 569 570 571 572 573 574 575 576 577 578 579 580 581 582 583 584 585 586 587 588 589 590 591 592 593 594 595 596 597 598 599 600 601 602 603 604 605 606 607 608 609 610 611 612 613 614 615 616 617 618 619 620 621 622 623 624 625 626 627 628 629 630 631 632 633 634 635 636 637 638 639 640 641 642 643 644 645 646 647 648 649 650 651 652 653 654 655 656 657 658 659 660 661 662 663 664 665 666 667 668 669 670 671 672 673 674 675 676 677 678 679 680 681 682 683 684 685 686 687 688 689 690 691 692 693 694 695 696 697 698 699 700 701 702 703 704 705 706 707 708 709 710 711 712 713 714 715 716 717 718 719 720 721 722 723 724 725 726 727 728 729 730 731 732 733 734 735 736 737 738 739 740 741 742 743 744 745 746 747 748 749 750 751 752 753 754 755 756 757 758 759 760 761 762 763 764 765 766 767 768 769 770 771 772 773 774 775 776 777 778 779 780 781 782 783 784 785 786 787 788 789 790 791 792 793 794 795 796 797 798 799 800 801 802 803 804 805 806 807 808 809 810 811 812 813 814 815 816 817 818 819 820 821 822 823 824 825 826 827 828 829 830 831 832 833 834 835 836 837 838 839 840 841 842 843 844 845 846 847 848 849 850 851 852 853 854 855 856 857 858 859 860 861 862 863 864 865 866 867 868 869 870 871 872 873 874 875 876 877 878 879 880 881 882 883 884 885 886 887 888 889 890 891 892 893 894 895 896 897 898 899 900 901 902 903 904 905 906 907 908 909 910 911 912 913 914 915 916 917 918 919 920 921 922 923 924 925 926 927 928 929 930 931 932 933 934 935 936 937 938 939 940 941 942 943 944 945 946 947 948 949 950 951 952 953 954 955 956 957 958 959 960 961 962 963 964 965 966 967 968 969 970 971 972 973 974 975 976 977 978 979 980 981 982 983 984 985 986 987 988 989 990 991 992 993 994 995 996 997 998 999 1000 1001 1002 1003 1004 1005 1006 1007 1008 1009 1010 1011 1012 1013 1014 1015 1016 1017 1018 1019 1020 1021 1022 1023 1024 1025 1026 1027 1028 1029 1030 1031 1032 1033 1034 1035 1036 1037 1038 1039 1040 1041 1042 1043 1044 1045 1046 1047 1048 1049 1050 1051 1052 1053 1054 1055 1056 1057 1058 1059 1060 1061 1062 1063 1064 1065 1066 1067 1068 1069 1070 1071 1072 1073 1074 1075 1076 1077 1078 1079 1080 1081 1082 1083 1084 1085 1086 1087 1088 1089 1090 1091 1092 1093 1094 1095 1096 1097 1098 1099 1100 1101 1102 1103 1104 1105 1106 1107 1108 1109 1110 1111 1112 1113 1114 1115 1116 1117 1118 1119 1120 1121 1122 1123 1124 1125 1126 1127 1128 1129 1130 1131 1132 1133 1134 1135 1136 1137 1138 1139 1140 1141 1142 1143 1144 1145 1146 1147 1148 1149 1150 1151 1152 1153 1154 1155 1156 1157 1158 1159 1160 1161 1162 1163 1164 1165 1166 1167 1168 1169 1170 1171 1172 1173 1174 1175 1176 1177 1178 1179 1180 1181 1182 1183 1184 1185 1186 1187 1188 1189 1190 1191 1192 1193 1194 1195 1196 1197 1198 1199 1200 1201 1202 1203 1204 1205 1206 1207 1208 1209 1210 1211 1212 1213 1214 1215 1216 1217 1218 1219 1220 1221 1222 1223 1224 1225 1226 1227 1228 1229 1230 1231 1232 1233 1234 1235 1236 1237 1238 1239 1240 1241 1242 1243 1244 1245 1246 1247 1248 1249 1250 1251 1252 1253 1254 1255 1256 1257 1258 1259 1260 1261 1262 1263 1264 1265 1266 1267 1268 1269 1270 1271
|
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You 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.apache.catalina.startup;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.MalformedURLException;
import java.net.URL;
import java.net.URLConnection;
import java.security.Principal;
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.jar.JarEntry;
import java.util.jar.JarFile;
import java.util.logging.Level;
import java.util.logging.LogManager;
import java.util.logging.Logger;
import jakarta.servlet.Servlet;
import jakarta.servlet.ServletException;
import jakarta.servlet.annotation.WebServlet;
import org.apache.catalina.Container;
import org.apache.catalina.Context;
import org.apache.catalina.Engine;
import org.apache.catalina.Globals;
import org.apache.catalina.Host;
import org.apache.catalina.Lifecycle;
import org.apache.catalina.LifecycleEvent;
import org.apache.catalina.LifecycleException;
import org.apache.catalina.LifecycleListener;
import org.apache.catalina.Realm;
import org.apache.catalina.Server;
import org.apache.catalina.Service;
import org.apache.catalina.Wrapper;
import org.apache.catalina.authenticator.NonLoginAuthenticator;
import org.apache.catalina.connector.Connector;
import org.apache.catalina.core.ContainerBase;
import org.apache.catalina.core.NamingContextListener;
import org.apache.catalina.core.StandardContext;
import org.apache.catalina.core.StandardEngine;
import org.apache.catalina.core.StandardHost;
import org.apache.catalina.core.StandardServer;
import org.apache.catalina.core.StandardService;
import org.apache.catalina.core.StandardWrapper;
import org.apache.catalina.realm.GenericPrincipal;
import org.apache.catalina.realm.RealmBase;
import org.apache.catalina.util.ContextName;
import org.apache.catalina.util.IOTools;
import org.apache.tomcat.util.ExceptionUtils;
import org.apache.tomcat.util.buf.UriUtil;
import org.apache.tomcat.util.compat.JreCompat;
import org.apache.tomcat.util.descriptor.web.LoginConfig;
import org.apache.tomcat.util.file.ConfigFileLoader;
import org.apache.tomcat.util.file.ConfigurationSource;
import org.apache.tomcat.util.modeler.Registry;
import org.apache.tomcat.util.res.StringManager;
// TODO: lazy init for the temp dir - only when a JSP is compiled or
// get temp dir is called we need to create it. This will avoid the
// need for the baseDir
// TODO: allow contexts without a base dir - i.e.
// only programmatic. This would disable the default servlet.
/**
* Minimal tomcat starter for embedding/unit tests.
* <p>
* Tomcat supports multiple styles of configuration and startup - the most common and stable is server.xml-based,
* implemented in org.apache.catalina.startup.Bootstrap.
* <p>
* This class is for use in apps that embed tomcat.
* <p>
* Requirements:
* <ul>
* <li>all tomcat classes and possibly servlets are in the classpath. (for example all is in one big jar, or in eclipse
* CP, or in any other combination)</li>
* <li>we need one temporary directory for work files</li>
* <li>no config file is required. This class provides methods to use if you have a webapp with a web.xml file, but it
* is optional - you can use your own servlets.</li>
* </ul>
* <p>
* There are a variety of 'add' methods to configure servlets and webapps. These methods, by default, create a simple
* in-memory security realm and apply it. If you need more complex security processing, you can define a subclass of
* this class.
* <p>
* This class provides a set of convenience methods for configuring web application contexts; all overloads of the
* method <code>addWebapp()</code>. These methods are equivalent to adding a web application to the Host's appBase
* (normally the webapps directory). These methods create a Context, configure it with the equivalent of the defaults
* provided by <code>conf/web.xml</code> (see {@link #initWebappDefaults(String)} for details) and add the Context to a
* Host. These methods do not use a global default web.xml; rather, they add a {@link LifecycleListener} to configure
* the defaults. Any WEB-INF/web.xml and META-INF/context.xml packaged with the application will be processed normally.
* Normal web fragment and {@link jakarta.servlet.ServletContainerInitializer} processing will be applied.
* <p>
* In complex cases, you may prefer to use the ordinary Tomcat API to create webapp contexts; for example, you might
* need to install a custom Loader before the call to {@link Host#addChild(Container)}. To replicate the basic behavior
* of the <code>addWebapp</code> methods, you may want to call two methods of this class: {@link #noDefaultWebXmlPath()}
* and {@link #getDefaultWebXmlListener()}.
* <p>
* {@link #getDefaultWebXmlListener()} returns a {@link LifecycleListener} that adds the standard DefaultServlet, JSP
* processing, and welcome files. If you add this listener, you must prevent Tomcat from applying any standard global
* web.xml with ...
* <p>
* {@link #noDefaultWebXmlPath()} returns a dummy pathname to configure to prevent {@link ContextConfig} from trying to
* apply a global web.xml file.
* <p>
* This class provides a main() and few simple CLI arguments, see setters for doc. It can be used for simple tests and
* demo.
*
* @see <a href=
* "https://gitbox.apache.org/repos/asf?p=tomcat.git;a=blob;f=test/org/apache/catalina/startup/TestTomcat.java">TestTomcat</a>
*/
public class Tomcat {
private static final StringManager sm = StringManager.getManager(Tomcat.class);
// Some logging implementations use weak references for loggers so there is
// the possibility that logging configuration could be lost if GC runs just
// after Loggers are configured but before they are used. The purpose of
// this Map is to retain strong references to explicitly configured loggers
// so that configuration is not lost.
private final Map<String,Logger> pinnedLoggers = new HashMap<>();
protected Server server;
protected int port = 8080;
protected String hostname = "localhost";
protected String basedir;
private final Map<String,String> userPass = new HashMap<>();
private final Map<String,List<String>> userRoles = new HashMap<>();
private final Map<String,Principal> userPrincipals = new HashMap<>();
private boolean addDefaultWebXmlToWebapp = true;
public Tomcat() {
ExceptionUtils.preload();
}
/**
* Tomcat requires that the base directory is set because the defaults for a number of other locations, such as the
* work directory, are derived from the base directory. This should be the first method called.
* <p>
* If this method is not called then Tomcat will attempt to use these locations in the following order:
* <ol>
* <li>if set, the catalina.base system property</li>
* <li>if set, the catalina.home system property</li>
* <li>The user.dir system property (the directory where Java was run from) where a directory named tomcat.$PORT
* will be created. $PORT is the value configured via {@link #setPort(int)} which defaults to 8080 if not set</li>
* </ol>
* The user should ensure that the file permissions for the base directory are appropriate.
* <p>
* TODO: disable work dir if not needed ( no jsp, etc ).
*
* @param basedir The Tomcat base folder on which all others will be derived
*/
public void setBaseDir(String basedir) {
this.basedir = basedir;
}
/**
* Set the port for the default connector. The default connector will only be created if getConnector is called.
*
* @param port The port number
*/
public void setPort(int port) {
this.port = port;
}
/**
* The hostname of the default host, default is 'localhost'.
*
* @param s The default host name
*/
public void setHostname(String s) {
hostname = s;
}
/**
* This is equivalent to adding a web application to a Host's appBase (usually Tomcat's webapps directory). By
* default, the equivalent of the default web.xml will be applied to the web application (see
* {@link #initWebappDefaults(String)}). This may be prevented by calling
* {@link #setAddDefaultWebXmlToWebapp(boolean)} with {@code false}. Any <code>WEB-INF/web.xml</code> and
* <code>META-INF/context.xml</code> packaged with the application will always be processed and normal web fragment
* and {@link jakarta.servlet.ServletContainerInitializer} processing will always be applied.
*
* @param contextPath The context mapping to use, "" for root context.
* @param docBase Base directory for the context, for static files. Must exist and be an absolute path.
*
* @return the deployed context
*/
public Context addWebapp(String contextPath, String docBase) {
return addWebapp(getHost(), contextPath, docBase);
}
/**
* Copy the specified WAR file to the Host's appBase and then call {@link #addWebapp(String, String)} with the newly
* copied WAR. The WAR will <b>NOT</b> be removed from the Host's appBase when the Tomcat instance stops. Note that
* {@link ExpandWar} provides utility methods that may be used to delete the WAR and/or expanded directory if
* required.
*
* @param contextPath The context mapping to use, "" for root context.
* @param source The location from which the WAR should be copied
*
* @return The deployed Context
*
* @throws IOException If an I/O error occurs while copying the WAR file from the specified URL to the appBase
*/
public Context addWebapp(String contextPath, URL source) throws IOException {
ContextName cn = new ContextName(contextPath, null);
// Make sure a conflicting web application has not already been deployed
Host h = getHost();
if (h.findChild(cn.getName()) != null) {
throw new IllegalArgumentException(
sm.getString("tomcat.addWebapp.conflictChild", source, contextPath, cn.getName()));
}
// Make sure appBase does not contain a conflicting web application
File targetWar = new File(h.getAppBaseFile(), cn.getBaseName() + ".war");
File targetDir = new File(h.getAppBaseFile(), cn.getBaseName());
if (targetWar.exists()) {
throw new IllegalArgumentException(
sm.getString("tomcat.addWebapp.conflictFile", source, contextPath, targetWar.getAbsolutePath()));
}
if (targetDir.exists()) {
throw new IllegalArgumentException(
sm.getString("tomcat.addWebapp.conflictFile", source, contextPath, targetDir.getAbsolutePath()));
}
// Should be good to copy the WAR now
URLConnection uConn = source.openConnection();
try (InputStream is = uConn.getInputStream(); OutputStream os = new FileOutputStream(targetWar)) {
IOTools.flow(is, os);
}
return addWebapp(contextPath, targetWar.getAbsolutePath());
}
/**
* Add a context - programmatic mode, no default web.xml used. This means that there is no JSP support (no JSP
* servlet), no default servlet and no web socket support unless explicitly enabled via the programmatic interface.
* There is also no {@link jakarta.servlet.ServletContainerInitializer} processing and no annotation processing. If
* a {@link jakarta.servlet.ServletContainerInitializer} is added programmatically, there will still be no scanning
* for {@link jakarta.servlet.annotation.HandlesTypes} matches.
* <p>
* API calls equivalent with web.xml:
*
* <pre>{@code
* // context-param
* ctx.addParameter("name", "value");
*
*
* // error-page
* ErrorPage ep = new ErrorPage();
* ep.setErrorCode(500);
* ep.setLocation("/error.html");
* ctx.addErrorPage(ep);
*
* ctx.addMimeMapping("ext", "type");
* }</pre>
* <p>
* Note: If you reload the Context, all your configuration will be lost. If you need reload support, consider using
* a LifecycleListener to provide your configuration.
* <p>
* TODO: add the rest
*
* @param contextPath The context mapping to use, "" for root context.
* @param docBase Base directory for the context, for static files. Must exist, relative to the server home
*
* @return the deployed context
*/
public Context addContext(String contextPath, String docBase) {
return addContext(getHost(), contextPath, docBase);
}
/**
* Equivalent to <servlet><servlet-name><servlet-class>.
* <p>
* In general it is better/faster to use the method that takes a Servlet as param - this one can be used if the
* servlet is not commonly used, and want to avoid loading all deps. ( for example: jsp servlet ) You can customize
* the returned servlet, ex:
*
* <pre>
* wrapper.addInitParameter("name", "value");
* </pre>
*
* @param contextPath Context to add Servlet to
* @param servletName Servlet name (used in mappings)
* @param servletClass The class to be used for the Servlet
*
* @return The wrapper for the servlet
*/
public Wrapper addServlet(String contextPath, String servletName, String servletClass) {
Container ctx = getHost().findChild(contextPath);
return addServlet((Context) ctx, servletName, servletClass);
}
/**
* Static version of {@link #addServlet(String, String, String)}
*
* @param ctx Context to add Servlet to
* @param servletName Servlet name (used in mappings)
* @param servletClass The class to be used for the Servlet
*
* @return The wrapper for the servlet
*/
public static Wrapper addServlet(Context ctx, String servletName, String servletClass) {
// will do class for name and set init params
Wrapper sw = ctx.createWrapper();
if (sw == null) {
throw new IllegalStateException(sm.getString("tomcat.noWrapper"));
}
sw.setServletClass(servletClass);
sw.setName(servletName);
ctx.addChild(sw);
return sw;
}
/**
* Add an existing Servlet to the context with no class.forName or initialisation.
*
* @param contextPath Context to add Servlet to
* @param servletName Servlet name (used in mappings)
* @param servlet The Servlet to add
*
* @return The wrapper for the servlet
*/
public Wrapper addServlet(String contextPath, String servletName, Servlet servlet) {
Container ctx = getHost().findChild(contextPath);
return addServlet((Context) ctx, servletName, servlet);
}
/**
* Static version of {@link #addServlet(String, String, Servlet)}.
*
* @param ctx Context to add Servlet to
* @param servletName Servlet name (used in mappings)
* @param servlet The Servlet to add
*
* @return The wrapper for the servlet
*/
public static Wrapper addServlet(Context ctx, String servletName, Servlet servlet) {
// will do class for name and set init params
Wrapper sw = new ExistingStandardWrapper(servlet);
sw.setName(servletName);
ctx.addChild(sw);
return sw;
}
/**
* Initialize the server given the specified configuration source. The server will be loaded according to the Tomcat
* configuration files contained in the source (server.xml, web.xml, context.xml, SSL certificates, etc). If no
* configuration source is specified, it will use the default locations for these files.
*
* @param source The configuration source
*/
public void init(ConfigurationSource source) {
init(source, null);
}
/**
* Initialize the server given the specified configuration source. The server will be loaded according to the Tomcat
* configuration files contained in the source (server.xml, web.xml, context.xml, SSL certificates, etc). If no
* configuration source is specified, it will use the default locations for these files.
*
* @param source The configuration source
* @param catalinaArguments The arguments that should be passed to Catalina
*/
public void init(ConfigurationSource source, String[] catalinaArguments) {
ConfigFileLoader.setSource(source);
addDefaultWebXmlToWebapp = false;
Catalina catalina = new Catalina();
// Load the Catalina instance with the regular configuration files
// from specified source
if (catalinaArguments == null) {
catalina.load();
} else {
catalina.load(catalinaArguments);
}
// Retrieve and set the server
server = catalina.getServer();
}
/**
* Initialize the server.
*
* @throws LifecycleException Init error
*/
public void init() throws LifecycleException {
getServer();
server.init();
}
/**
* Start the server.
*
* @throws LifecycleException Start error
*/
public void start() throws LifecycleException {
getServer();
server.start();
}
/**
* Stop the server.
*
* @throws LifecycleException Stop error
*/
public void stop() throws LifecycleException {
getServer();
server.stop();
}
/**
* Destroy the server. This object cannot be used once this method has been called.
*
* @throws LifecycleException Destroy error
*/
public void destroy() throws LifecycleException {
getServer();
server.destroy();
// Could null out objects here
}
/**
* Add a user for the in-memory realm. All created apps use this by default, can be replaced using setRealm().
*
* @param user The username
* @param pass The password
*/
public void addUser(String user, String pass) {
userPass.put(user, pass);
}
/**
* Add a role to a user.
*
* @see #addUser(String, String)
*
* @param user The username
* @param role The role name
*/
public void addRole(String user, String role) {
userRoles.computeIfAbsent(user, k -> new ArrayList<>()).add(role);
}
// ------- Extra customization -------
// You can tune individual Tomcat objects, using internal APIs
/**
* Get the default HTTP connector that is used by the embedded Tomcat. It is first configured connector in the
* service. If there's no connector defined, it will create and add a default connector using the port and address
* specified in this Tomcat instance, and return it for further customization.
*
* @return The connector object
*/
public Connector getConnector() {
Service service = getService();
if (service.findConnectors().length > 0) {
return service.findConnectors()[0];
}
// The same as in standard Tomcat configuration.
// This creates a NIO HTTP connector.
Connector connector = new Connector("HTTP/1.1");
connector.setPort(port);
service.addConnector(connector);
return connector;
}
/**
* Set the specified connector in the service, if it is not already present.
*
* @param connector The connector instance to add
*/
public void setConnector(Connector connector) {
Service service = getService();
boolean found = false;
for (Connector serviceConnector : service.findConnectors()) {
if (connector == serviceConnector) {
found = true;
break;
}
}
if (!found) {
service.addConnector(connector);
}
}
/**
* Get the service object. Can be used to add more connectors and few other global settings.
*
* @return The service
*/
public Service getService() {
return getServer().findServices()[0];
}
/**
* Sets the current host - all future webapps will be added to this host. When tomcat starts, the host will be the
* default host.
*
* @param host The current host
*/
public void setHost(Host host) {
Engine engine = getEngine();
boolean found = false;
for (Container engineHost : engine.findChildren()) {
if (engineHost == host) {
found = true;
break;
}
}
if (!found) {
engine.addChild(host);
}
}
public Host getHost() {
Engine engine = getEngine();
if (engine.findChildren().length > 0) {
return (Host) engine.findChildren()[0];
}
Host host = new StandardHost();
host.setName(hostname);
getEngine().addChild(host);
return host;
}
/**
* Access to the engine, for further customization.
*
* @return The engine
*/
public Engine getEngine() {
Service service = getServer().findServices()[0];
if (service.getContainer() != null) {
return service.getContainer();
}
Engine engine = new StandardEngine();
engine.setName("Tomcat");
engine.setDefaultHost(hostname);
engine.setRealm(createDefaultRealm());
service.setContainer(engine);
return engine;
}
/**
* Get the server object. You can add listeners and few more customizations. JNDI is disabled by default.
*
* @return The Server
*/
public Server getServer() {
if (server != null) {
return server;
}
System.setProperty("catalina.useNaming", "false");
server = new StandardServer();
initBaseDir();
// Set configuration source
ConfigFileLoader.setSource(new CatalinaBaseConfigurationSource(new File(basedir), null));
server.setPort(-1);
Service service = new StandardService();
service.setName("Tomcat");
server.addService(service);
return server;
}
/**
* @param host The host in which the context will be deployed
* @param contextPath The context mapping to use, "" for root context.
* @param dir Base directory for the context, for static files. Must exist, relative to the server home
*
* @return the deployed context
*
* @see #addContext(String, String)
*/
public Context addContext(Host host, String contextPath, String dir) {
return addContext(host, contextPath, contextPath, dir);
}
/**
* @param host The host in which the context will be deployed
* @param contextPath The context mapping to use, "" for root context.
* @param contextName The context name
* @param dir Base directory for the context, for static files. Must exist, relative to the server home
*
* @return the deployed context
*
* @see #addContext(String, String)
*/
public Context addContext(Host host, String contextPath, String contextName, String dir) {
silence(host, contextName);
Context ctx = createContext(host, contextPath);
ctx.setName(contextName);
ctx.setPath(contextPath);
ctx.setDocBase(dir);
ctx.addLifecycleListener(new FixContextListener());
if (host == null) {
getHost().addChild(ctx);
} else {
host.addChild(ctx);
}
return ctx;
}
/**
* This is equivalent to adding a web application to a Host's appBase (usually Tomcat's webapps directory). By
* default, the equivalent of the default web.xml will be applied to the web application (see
* {@link #initWebappDefaults(String)}). This may be prevented by calling
* {@link #setAddDefaultWebXmlToWebapp(boolean)} with {@code false}. Any <code>WEB-INF/web.xml</code> and
* <code>META-INF/context.xml</code> packaged with the application will always be processed and normal web fragment
* and {@link jakarta.servlet.ServletContainerInitializer} processing will always be applied.
*
* @param host The host in which the context will be deployed
* @param contextPath The context mapping to use, "" for root context.
* @param docBase Base directory for the context, for static files. Must exist and be an absolute path.
*
* @return the deployed context
*/
public Context addWebapp(Host host, String contextPath, String docBase) {
LifecycleListener listener;
try {
Class<?> clazz = Class.forName(getHost().getConfigClass());
listener = (LifecycleListener) clazz.getConstructor().newInstance();
} catch (ReflectiveOperationException e) {
// Wrap in IAE since we can't easily change the method signature
// to throw the specific checked exceptions
throw new IllegalArgumentException(e);
}
return addWebapp(host, contextPath, docBase, listener);
}
/**
* This is equivalent to adding a web application to a Host's appBase (usually Tomcat's webapps directory). By
* default, the equivalent of the default web.xml will be applied to the web application (see
* {@link #initWebappDefaults(String)}). This may be prevented by calling
* {@link #setAddDefaultWebXmlToWebapp(boolean)} with {@code false}. Any <code>WEB-INF/web.xml</code> and
* <code>META-INF/context.xml</code> packaged with the application will always be processed and normal web fragment
* and {@link jakarta.servlet.ServletContainerInitializer} processing will always be applied.
*
* @param host The host in which the context will be deployed
* @param contextPath The context mapping to use, "" for root context.
* @param docBase Base directory for the context, for static files. Must exist and be an absolute path.
* @param config Custom context configuration helper. Any configuration will be in addition to equivalent of
* the default web.xml configuration described above.
*
* @return the deployed context
*/
public Context addWebapp(Host host, String contextPath, String docBase, LifecycleListener config) {
silence(host, contextPath);
Context ctx = createContext(host, contextPath);
ctx.setPath(contextPath);
ctx.setDocBase(docBase);
if (addDefaultWebXmlToWebapp) {
ctx.addLifecycleListener(getDefaultWebXmlListener());
}
ctx.setConfigFile(getWebappConfigFile(docBase, contextPath));
ctx.addLifecycleListener(config);
if (addDefaultWebXmlToWebapp && (config instanceof ContextConfig)) {
// prevent it from looking ( if it finds one - it'll have dup error )
((ContextConfig) config).setDefaultWebXml(noDefaultWebXmlPath());
}
if (host == null) {
getHost().addChild(ctx);
} else {
host.addChild(ctx);
}
return ctx;
}
/**
* Return a listener that provides the required configuration items for JSP processing. From the standard Tomcat
* global web.xml. Pass this to {@link Context#addLifecycleListener(LifecycleListener)} and then pass the result of
* {@link #noDefaultWebXmlPath()} to {@link ContextConfig#setDefaultWebXml(String)}.
*
* @return a listener object that configures default JSP processing.
*/
public LifecycleListener getDefaultWebXmlListener() {
return new DefaultWebXmlListener();
}
/**
* @return a pathname to pass to {@link ContextConfig#setDefaultWebXml(String)} when using
* {@link #getDefaultWebXmlListener()}.
*/
public String noDefaultWebXmlPath() {
return Constants.NoDefaultWebXml;
}
// ---------- Helper methods and classes -------------------
/**
* Create an in-memory realm. You can replace it for contexts with a real one. The Realm created here will be added
* to the Engine by default and may be replaced at the Engine level or over-ridden (as per normal Tomcat behaviour)
* at the Host or Context level.
*
* @return a realm instance
*/
protected Realm createDefaultRealm() {
return new SimpleRealm();
}
private class SimpleRealm extends RealmBase {
@Override
protected String getPassword(String username) {
return userPass.get(username);
}
@Override
protected Principal getPrincipal(String username) {
Principal p = userPrincipals.get(username);
if (p == null) {
String pass = userPass.get(username);
if (pass != null) {
p = new GenericPrincipal(username, userRoles.get(username));
userPrincipals.put(username, p);
}
}
return p;
}
}
protected void initBaseDir() {
String catalinaHome = System.getProperty(Globals.CATALINA_HOME_PROP);
if (basedir == null) {
basedir = System.getProperty(Globals.CATALINA_BASE_PROP);
}
if (basedir == null) {
basedir = catalinaHome;
}
if (basedir == null) {
// Create a temp dir.
basedir = System.getProperty("user.dir") + "/tomcat." + port;
}
File baseFile = new File(basedir);
if (baseFile.exists()) {
if (!baseFile.isDirectory()) {
throw new IllegalArgumentException(sm.getString("tomcat.baseDirNotDir", baseFile));
}
} else {
if (!baseFile.mkdirs()) {
// Failed to create base directory
throw new IllegalStateException(sm.getString("tomcat.baseDirMakeFail", baseFile));
}
/*
* If file permissions were going to be set on the newly created directory, this is the place to do it.
* However, even simple calls such as File.setReadable(boolean,boolean) behaves differently on different
* platforms. Therefore, setBaseDir documents that the user needs to do this.
*/
}
try {
baseFile = baseFile.getCanonicalFile();
} catch (IOException ioe) {
baseFile = baseFile.getAbsoluteFile();
}
server.setCatalinaBase(baseFile);
System.setProperty(Globals.CATALINA_BASE_PROP, baseFile.getPath());
basedir = baseFile.getPath();
if (catalinaHome == null) {
server.setCatalinaHome(baseFile);
} else {
File homeFile = new File(catalinaHome);
if (!homeFile.isDirectory() && !homeFile.mkdirs()) {
// Failed to create home directory
throw new IllegalStateException(sm.getString("tomcat.homeDirMakeFail", homeFile));
}
try {
homeFile = homeFile.getCanonicalFile();
} catch (IOException ioe) {
homeFile = homeFile.getAbsoluteFile();
}
server.setCatalinaHome(homeFile);
}
System.setProperty(Globals.CATALINA_HOME_PROP, server.getCatalinaHome().getPath());
}
static final String[] silences = new String[] { "org.apache.coyote.http11.Http11NioProtocol",
"org.apache.catalina.core.StandardService", "org.apache.catalina.core.StandardEngine",
"org.apache.catalina.startup.ContextConfig", "org.apache.catalina.core.ApplicationContext",
"org.apache.catalina.core.AprLifecycleListener", "org.apache.catalina.core.OpenSSLLifecycleListener" };
private boolean silent = false;
/**
* Controls if the loggers will be silenced or not.
*
* @param silent <code>true</code> sets the log level to WARN for the loggers that log information on Tomcat start
* up. This prevents the usual startup information being logged. <code>false</code> sets the log
* level to the default level of INFO.
*/
public void setSilent(boolean silent) {
this.silent = silent;
for (String s : silences) {
Logger logger = Logger.getLogger(s);
pinnedLoggers.put(s, logger);
if (silent) {
logger.setLevel(Level.WARNING);
} else {
logger.setLevel(Level.INFO);
}
}
}
private void silence(Host host, String contextPath) {
String loggerName = getLoggerName(host, contextPath);
Logger logger = Logger.getLogger(loggerName);
pinnedLoggers.put(loggerName, logger);
if (silent) {
logger.setLevel(Level.WARNING);
} else {
logger.setLevel(Level.INFO);
}
}
/**
* By default, when calling addWebapp() to create a Context, the settings from the default web.xml are added to the
* context. Calling this method with a <code>false</code> value prior to calling addWebapp() allows to opt out of
* the default settings. In that event you will need to add the configurations yourself, either programmatically or
* by using web.xml deployment descriptors.
*
* @param addDefaultWebXmlToWebapp <code>false</code> will prevent the class from automatically adding the default
* settings when calling addWebapp(). <code>true</code> will add the default
* settings and is the default behavior.
*
* @see #addWebapp(Host, String, String, LifecycleListener)
*/
public void setAddDefaultWebXmlToWebapp(boolean addDefaultWebXmlToWebapp) {
this.addDefaultWebXmlToWebapp = addDefaultWebXmlToWebapp;
}
/*
* Uses essentially the same logic as {@link ContainerBase#logName()}.
*/
private String getLoggerName(Host host, String contextName) {
if (host == null) {
host = getHost();
}
StringBuilder loggerName = new StringBuilder();
loggerName.append(ContainerBase.class.getName());
loggerName.append(".[");
// Engine name
loggerName.append(host.getParent().getName());
loggerName.append("].[");
// Host name
loggerName.append(host.getName());
loggerName.append("].[");
// Context name
if (contextName == null || contextName.isEmpty()) {
loggerName.append('/');
} else if (contextName.startsWith("##")) {
loggerName.append('/');
loggerName.append(contextName);
}
loggerName.append(']');
return loggerName.toString();
}
/**
* Create the configured {@link Context} for the given <code>host</code>. The default constructor of the class that
* was configured with {@link StandardHost#setContextClass(String)} will be used
*
* @param host host for which the {@link Context} should be created, or <code>null</code> if default host should be
* used
* @param url path of the webapp which should get the {@link Context}
*
* @return newly created {@link Context}
*/
private Context createContext(Host host, String url) {
String defaultContextClass = StandardContext.class.getName();
String contextClass = StandardContext.class.getName();
if (host == null) {
host = this.getHost();
}
if (host instanceof StandardHost) {
contextClass = ((StandardHost) host).getContextClass();
}
try {
if (defaultContextClass.equals(contextClass)) {
return new StandardContext();
} else {
return (Context) Class.forName(contextClass).getConstructor().newInstance();
}
} catch (ReflectiveOperationException | IllegalArgumentException | SecurityException e) {
throw new IllegalArgumentException(sm.getString("tomcat.noContextClass", contextClass, host, url), e);
}
}
/**
* Enables JNDI naming which is disabled by default. Server must implement {@link Lifecycle} in order for the
* {@link NamingContextListener} to be used.
*/
public void enableNaming() {
// Make sure getServer() has been called as that is where naming is
// disabled
getServer();
server.addLifecycleListener(new NamingContextListener());
System.setProperty("catalina.useNaming", "true");
String value = "org.apache.naming";
String oldValue = System.getProperty(javax.naming.Context.URL_PKG_PREFIXES);
if (oldValue != null) {
if (oldValue.contains(value)) {
value = oldValue;
} else {
value = value + ":" + oldValue;
}
}
System.setProperty(javax.naming.Context.URL_PKG_PREFIXES, value);
value = System.getProperty(javax.naming.Context.INITIAL_CONTEXT_FACTORY);
if (value == null) {
System.setProperty(javax.naming.Context.INITIAL_CONTEXT_FACTORY,
"org.apache.naming.java.javaURLContextFactory");
}
}
/**
* Provide default configuration for a context. This is broadly the programmatic equivalent of the default web.xml
* and provides the following features:
* <ul>
* <li>Default servlet mapped to "/"</li>
* <li>JSP servlet mapped to "*.jsp" and ""*.jspx"</li>
* <li>Session timeout of 30 minutes</li>
* <li>MIME mappings (subset of those in conf/web.xml)</li>
* <li>Welcome files</li>
* </ul>
*
* @param contextPath The path of the context to set the defaults for
*/
public void initWebappDefaults(String contextPath) {
Container ctx = getHost().findChild(contextPath);
initWebappDefaults((Context) ctx);
}
/**
* Static version of {@link #initWebappDefaults(String)}.
*
* @param ctx The context to set the defaults for
*/
public static void initWebappDefaults(Context ctx) {
// Default servlet
Wrapper servlet = addServlet(ctx, "default", "org.apache.catalina.servlets.DefaultServlet");
servlet.setLoadOnStartup(1);
servlet.setOverridable(true);
// JSP servlet (by class name - to avoid loading all deps)
servlet = addServlet(ctx, "jsp", "org.apache.jasper.servlet.JspServlet");
servlet.addInitParameter("fork", "false");
servlet.setLoadOnStartup(3);
servlet.setOverridable(true);
// Servlet mappings
ctx.addServletMappingDecoded("/", "default");
ctx.addServletMappingDecoded("*.jsp", "jsp");
ctx.addServletMappingDecoded("*.jspx", "jsp");
// Sessions
ctx.setSessionTimeout(30);
// MIME type mappings
addDefaultMimeTypeMappings(ctx);
// Welcome files
ctx.addWelcomeFile("index.html");
ctx.addWelcomeFile("index.htm");
ctx.addWelcomeFile("index.jsp");
// Any application configured welcome files should override the defaults.
if (ctx instanceof StandardContext stdCtx) {
stdCtx.setReplaceWelcomeFiles(true);
}
}
/**
* Add the default MIME type mappings to the provided Context.
*
* @param context The web application to which the default MIME type mappings should be added.
*/
public static void addDefaultMimeTypeMappings(Context context) {
Properties defaultMimeMappings = new Properties();
try (InputStream is = Tomcat.class.getResourceAsStream("MimeTypeMappings.properties")) {
defaultMimeMappings.load(is);
for (Map.Entry<Object,Object> entry : defaultMimeMappings.entrySet()) {
context.addMimeMapping((String) entry.getKey(), (String) entry.getValue());
}
} catch (IOException ioe) {
throw new IllegalStateException(sm.getString("tomcat.defaultMimeTypeMappingsFail"), ioe);
}
}
/**
* Fix startup sequence - required if you don't use web.xml.
* <p>
* The start() method in context will set 'configured' to false - and expects a listener to set it back to true.
*/
public static class FixContextListener implements LifecycleListener {
@Override
public void lifecycleEvent(LifecycleEvent event) {
try {
Context context = (Context) event.getLifecycle();
if (event.getType().equals(Lifecycle.CONFIGURE_START_EVENT)) {
context.setConfigured(true);
// Process annotations
WebAnnotationSet.loadApplicationAnnotations(context);
// LoginConfig is required to process @ServletSecurity
// annotations
if (context.getLoginConfig() == null) {
context.setLoginConfig(new LoginConfig("NONE", null, null, null));
context.getPipeline().addValve(new NonLoginAuthenticator());
}
}
} catch (ClassCastException e) {
// Ignore
}
}
}
/**
* Fix reload - required if reloading and using programmatic configuration. When a context is reloaded, any
* programmatic configuration is lost. This listener sets the equivalent of conf/web.xml when the context starts.
*/
public static class DefaultWebXmlListener implements LifecycleListener {
@Override
public void lifecycleEvent(LifecycleEvent event) {
if (Lifecycle.BEFORE_START_EVENT.equals(event.getType())) {
initWebappDefaults((Context) event.getLifecycle());
}
}
}
/**
* Helper class for wrapping existing servlets. This disables servlet lifecycle and normal reloading, but also
* reduces overhead and provide more direct control over the servlet.
*/
public static class ExistingStandardWrapper extends StandardWrapper {
private final Servlet existing;
public ExistingStandardWrapper(Servlet existing) {
this.existing = existing;
this.asyncSupported = hasAsync(existing);
}
private static boolean hasAsync(Servlet existing) {
boolean result = false;
Class<?> clazz = existing.getClass();
WebServlet ws = clazz.getAnnotation(WebServlet.class);
if (ws != null) {
result = ws.asyncSupported();
}
return result;
}
@Override
public synchronized Servlet loadServlet() throws ServletException {
if (!instanceInitialized) {
existing.init(facade);
instanceInitialized = true;
}
return existing;
}
@Override
public long getAvailable() {
return 0;
}
@Override
public boolean isUnavailable() {
return false;
}
@Override
public Servlet getServlet() {
return existing;
}
@Override
public String getServletClass() {
return existing.getClass().getName();
}
}
protected URL getWebappConfigFile(String path, String contextName) {
File docBase = new File(path);
if (docBase.isDirectory()) {
return getWebappConfigFileFromDirectory(docBase, contextName);
} else {
return getWebappConfigFileFromWar(docBase, contextName);
}
}
private URL getWebappConfigFileFromDirectory(File docBase, String contextName) {
URL result = null;
File webAppContextXml = new File(docBase, Constants.ApplicationContextXml);
if (webAppContextXml.exists()) {
try {
result = webAppContextXml.toURI().toURL();
} catch (MalformedURLException e) {
Logger.getLogger(getLoggerName(getHost(), contextName)).log(Level.WARNING,
sm.getString("tomcat.noContextXml", docBase), e);
}
}
return result;
}
private URL getWebappConfigFileFromWar(File docBase, String contextName) {
URL result = null;
try (JarFile jar = new JarFile(docBase)) {
JarEntry entry = jar.getJarEntry(Constants.ApplicationContextXml);
if (entry != null) {
result = UriUtil.buildJarUrl(docBase, Constants.ApplicationContextXml);
}
} catch (IOException ioe) {
Logger.getLogger(getLoggerName(getHost(), contextName)).log(Level.WARNING,
sm.getString("tomcat.noContextXml", docBase), ioe);
}
return result;
}
static {
// Graal native images don't load any configuration except the VM default
if (JreCompat.isGraalAvailable()) {
try (InputStream is = new FileInputStream(
System.getProperty("java.util.logging.config.file", "conf/logging.properties"))) {
LogManager.getLogManager().readConfiguration(is);
} catch (SecurityException | IOException e) {
// Ignore, the VM default will be used
}
}
}
/**
* Main executable method for use with a Maven packager.
*
* @param args the command line arguments
*
* @throws Exception if an error occurs
*/
public static void main(String[] args) throws Exception {
// Process some command line parameters
String[] catalinaArguments = null;
for (int i = 0; i < args.length; i++) {
if (args[i].equals("--no-jmx")) {
Registry.disableRegistry();
} else if (args[i].equals("--catalina")) {
// This was already processed before
// Skip the rest of the arguments as they are for Catalina
ArrayList<String> result = new ArrayList<>(Arrays.asList(args).subList(i + 1, args.length));
catalinaArguments = result.toArray(new String[0]);
break;
}
}
Tomcat tomcat = new Tomcat();
// Create a Catalina instance and let it parse the configuration files
// It will also set a shutdown hook to stop the Server when needed
// Use the default configuration source
tomcat.init(null, catalinaArguments);
boolean await = false;
String path = "";
// Process command line parameters
label:
for (int i = 0; i < args.length; i++) {
switch (args[i]) {
case "--war":
if (++i >= args.length) {
throw new IllegalArgumentException(sm.getString("tomcat.invalidCommandLine", args[i - 1]));
}
File war = new File(args[i]);
tomcat.addWebapp(path, war.getAbsolutePath());
break;
case "--path":
if (++i >= args.length) {
throw new IllegalArgumentException(sm.getString("tomcat.invalidCommandLine", args[i - 1]));
}
path = args[i];
break;
case "--await":
await = true;
break;
case "--no-jmx":
// This was already processed before
break;
case "--catalina":
// This was already processed before
// Skip the rest of the arguments as they are for Catalina
break label;
default:
throw new IllegalArgumentException(sm.getString("tomcat.invalidCommandLine", args[i]));
}
}
tomcat.start();
// Ideally the utility threads are non daemon
if (await) {
tomcat.getServer().await();
}
}
}
|