| /* |
| * 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> |
| * |
| * @author Costin Manolache |
| */ |
| 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(); |
| } |
| } |
| |
| } |