/* * 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.manager; import java.io.BufferedOutputStream; import java.io.File; import java.io.FileInputStream; import java.io.FileOutputStream; import java.io.IOException; import java.io.PrintWriter; import java.security.cert.Certificate; import java.security.cert.X509Certificate; import java.util.ArrayList; import java.util.Arrays; import java.util.Enumeration; import java.util.HashMap; import java.util.LinkedHashSet; import java.util.List; import java.util.Locale; import java.util.Map; import java.util.Set; import javax.management.MBeanServer; import javax.management.MalformedObjectNameException; import javax.management.ObjectName; import javax.naming.Binding; import javax.naming.NamingEnumeration; import javax.servlet.ServletContext; import javax.servlet.ServletException; import javax.servlet.ServletInputStream; import javax.servlet.UnavailableException; import javax.servlet.http.HttpServlet; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import org.apache.catalina.Container; import org.apache.catalina.ContainerServlet; import org.apache.catalina.Context; import org.apache.catalina.Engine; import org.apache.catalina.Host; import org.apache.catalina.Manager; import org.apache.catalina.Server; import org.apache.catalina.Service; import org.apache.catalina.Session; import org.apache.catalina.Wrapper; import org.apache.catalina.connector.Connector; import org.apache.catalina.core.StandardHost; import org.apache.catalina.startup.ExpandWar; import org.apache.catalina.util.ContextName; import org.apache.catalina.util.ServerInfo; import org.apache.coyote.ProtocolHandler; import org.apache.coyote.http11.AbstractHttp11Protocol; import org.apache.tomcat.util.Diagnostics; import org.apache.tomcat.util.ExceptionUtils; import org.apache.tomcat.util.IntrospectionUtils; import org.apache.tomcat.util.modeler.Registry; import org.apache.tomcat.util.net.SSLContext; import org.apache.tomcat.util.net.SSLHostConfig; import org.apache.tomcat.util.net.SSLHostConfigCertificate; import org.apache.tomcat.util.res.StringManager; import org.apache.tomcat.util.security.Escape; /** * Servlet that enables remote management of the web applications installed * within the same virtual host as this web application is. Normally, this * functionality will be protected by a security constraint in the web * application deployment descriptor. However, this requirement can be * relaxed during testing. *
* This servlet examines the value returned by getPathInfo()
* and related query parameters to determine what action is being requested.
* The following actions and parameters (starting after the servlet path)
* are supported:
*
docBase attribute
* of the context configuration file is used to locate the actual
* WAR or directory containing the application.{config-url}, overriding the
* docBase attribute with the contents of the web
* application archive found at {war-url}./xxx, based
* on the contents of the web application archive found at the
* specified URL.path:status:sessions.
* Where path is the context path. Status is either running or stopped.
* Sessions is the number of active Sessions./xxx for this
* virtual host./xxx which were idle for at
* least mm minutes./xxx for this virtual host./xxx for this virtual host./xxx for this virtual host,
* and remove the underlying WAR file or document base directory.
* (NOTE - This is only allowed if the WAR file or document
* base is stored in the appBase directory of this host,
* typically as a result of being placed there via the /deploy
* command./xxx to an appropriately
* named context.xml file in the xmlBase for the associated
* Host.Use path=/ for the ROOT context.
The syntax of the URL for a web application archive must conform to one * of the following patterns to be successfully deployed:
** NOTE - Attempting to reload or remove the application containing * this servlet itself will not succeed. Therefore, this servlet should * generally be deployed as a separate web application within the virtual host * to be managed. *
* The following servlet initialization parameters are recognized: *
NamingContext for this server,
* if available.
*/
protected transient javax.naming.Context global = null;
/**
* The string manager for this package.
*/
protected static final StringManager sm =
StringManager.getManager(Constants.Package);
/**
* The Wrapper container associated with this servlet.
*/
protected transient Wrapper wrapper = null;
// ----------------------------------------------- ContainerServlet Methods
/**
* Return the Wrapper with which we are associated.
*/
@Override
public Wrapper getWrapper() {
return this.wrapper;
}
/**
* Set the Wrapper with which we are associated.
*
* @param wrapper The new wrapper
*/
@Override
public void setWrapper(Wrapper wrapper) {
this.wrapper = wrapper;
if (wrapper == null) {
context = null;
host = null;
oname = null;
} else {
context = (Context) wrapper.getParent();
host = (Host) context.getParent();
Engine engine = (Engine) host.getParent();
String name = engine.getName() + ":type=Deployer,host=" +
host.getName();
try {
oname = new ObjectName(name);
} catch (Exception e) {
log(sm.getString("managerServlet.objectNameFail", name), e);
}
}
// Retrieve the MBean server
mBeanServer = Registry.getRegistry(null, null).getMBeanServer();
}
// --------------------------------------------------------- Public Methods
/**
* Finalize this servlet.
*/
@Override
public void destroy() {
// No actions necessary
}
/**
* Process a GET request for the specified resource.
*
* @param request The servlet request we are processing
* @param response The servlet response we are creating
*
* @exception IOException if an input/output error occurs
* @exception ServletException if a servlet-specified error occurs
*/
@Override
public void doGet(HttpServletRequest request,
HttpServletResponse response)
throws IOException, ServletException {
StringManager smClient = StringManager.getManager(
Constants.Package, request.getLocales());
// Identify the request parameters that we need
String command = request.getPathInfo();
if (command == null)
command = request.getServletPath();
String config = request.getParameter("config");
String path = request.getParameter("path");
ContextName cn = null;
if (path != null) {
cn = new ContextName(path, request.getParameter("version"));
}
String type = request.getParameter("type");
String war = request.getParameter("war");
String tag = request.getParameter("tag");
boolean update = false;
if ((request.getParameter("update") != null)
&& (request.getParameter("update").equals("true"))) {
update = true;
}
String tlsHostName = request.getParameter("tlsHostName");
boolean statusLine = false;
if ("true".equals(request.getParameter("statusLine"))) {
statusLine = true;
}
// Prepare our output writer to generate the response message
response.setContentType("text/plain; charset=" + Constants.CHARSET);
// Stop older versions of IE thinking they know best. We set text/plain
// in the line above for a reason. IE's behaviour is unwanted at best
// and dangerous at worst.
response.setHeader("X-Content-Type-Options", "nosniff");
PrintWriter writer = response.getWriter();
// Process the requested command
if (command == null) {
writer.println(smClient.getString("managerServlet.noCommand"));
} else if (command.equals("/deploy")) {
if (war != null || config != null) {
deploy(writer, config, cn, war, update, smClient);
} else if (tag != null) {
deploy(writer, cn, tag, smClient);
} else {
writer.println(smClient.getString(
"managerServlet.invalidCommand", command));
}
} else if (command.equals("/list")) {
list(writer, smClient);
} else if (command.equals("/reload")) {
reload(writer, cn, smClient);
} else if (command.equals("/resources")) {
resources(writer, type, smClient);
} else if (command.equals("/save")) {
save(writer, path, smClient);
} else if (command.equals("/serverinfo")) {
serverinfo(writer, smClient);
} else if (command.equals("/sessions")) {
expireSessions(writer, cn, request, smClient);
} else if (command.equals("/expire")) {
expireSessions(writer, cn, request, smClient);
} else if (command.equals("/start")) {
start(writer, cn, smClient);
} else if (command.equals("/stop")) {
stop(writer, cn, smClient);
} else if (command.equals("/undeploy")) {
undeploy(writer, cn, smClient);
} else if (command.equals("/findleaks")) {
findleaks(statusLine, writer, smClient);
} else if (command.equals("/vminfo")) {
vmInfo(writer, smClient, request.getLocales());
} else if (command.equals("/threaddump")) {
threadDump(writer, smClient, request.getLocales());
} else if (command.equals("/sslConnectorCiphers")) {
sslConnectorCiphers(writer, smClient);
} else if (command.equals("/sslConnectorCerts")) {
sslConnectorCerts(writer, smClient);
} else if (command.equals("/sslConnectorTrustedCerts")) {
sslConnectorTrustedCerts(writer, smClient);
} else if (command.equals("/sslReload")) {
sslReload(writer, tlsHostName, smClient);
} else {
writer.println(smClient.getString("managerServlet.unknownCommand",
command));
}
// Finish up the response
writer.flush();
writer.close();
}
/**
* Process a PUT request for the specified resource.
*
* @param request The servlet request we are processing
* @param response The servlet response we are creating
*
* @exception IOException if an input/output error occurs
* @exception ServletException if a servlet-specified error occurs
*/
@Override
public void doPut(HttpServletRequest request,
HttpServletResponse response)
throws IOException, ServletException {
StringManager smClient = StringManager.getManager(
Constants.Package, request.getLocales());
// Identify the request parameters that we need
String command = request.getPathInfo();
if (command == null)
command = request.getServletPath();
String path = request.getParameter("path");
ContextName cn = null;
if (path != null) {
cn = new ContextName(path, request.getParameter("version"));
}
String tag = request.getParameter("tag");
boolean update = false;
if ((request.getParameter("update") != null)
&& (request.getParameter("update").equals("true"))) {
update = true;
}
// Prepare our output writer to generate the response message
response.setContentType("text/plain;charset="+Constants.CHARSET);
// Stop older versions of IE thinking they know best. We set text/plain
// in the line above for a reason. IE's behaviour is unwanted at best
// and dangerous at worst.
response.setHeader("X-Content-Type-Options", "nosniff");
PrintWriter writer = response.getWriter();
// Process the requested command
if (command == null) {
writer.println(smClient.getString("managerServlet.noCommand"));
} else if (command.equals("/deploy")) {
deploy(writer, cn, tag, update, request, smClient);
} else {
writer.println(smClient.getString("managerServlet.unknownCommand",
command));
}
// Finish up the response
writer.flush();
writer.close();
}
/**
* Initialize this servlet.
*/
@Override
public void init() throws ServletException {
// Ensure that our ContainerServlet properties have been set
if ((wrapper == null) || (context == null))
throw new UnavailableException(
sm.getString("managerServlet.noWrapper"));
// Set our properties from the initialization parameters
String value = null;
try {
value = getServletConfig().getInitParameter("debug");
debug = Integer.parseInt(value);
} catch (Throwable t) {
ExceptionUtils.handleThrowable(t);
}
// Acquire global JNDI resources if available
Server server = ((Engine)host.getParent()).getService().getServer();
if (server != null) {
global = server.getGlobalNamingContext();
}
// Calculate the directory into which we will be deploying applications
versioned = (File) getServletContext().getAttribute
(ServletContext.TEMPDIR);
configBase = new File(context.getCatalinaBase(), "conf");
Container container = context;
Container host = null;
Container engine = null;
while (container != null) {
if (container instanceof Host)
host = container;
if (container instanceof Engine)
engine = container;
container = container.getParent();
}
if (engine != null) {
configBase = new File(configBase, engine.getName());
}
if (host != null) {
configBase = new File(configBase, host.getName());
}
// Note: The directory must exist for this to work.
// Log debugging messages as necessary
if (debug >= 1) {
log("init: Associated with Deployer '" +
oname + "'");
if (global != null) {
log("init: Global resources are available");
}
}
}
// -------------------------------------------------------- Private Methods
/**
* Find potential memory leaks caused by web application reload.
*
* @param statusLine Print a status line
* @param writer The output writer
* @param smClient StringManager for the client's locale
*/
protected void findleaks(boolean statusLine, PrintWriter writer,
StringManager smClient) {
if (!(host instanceof StandardHost)) {
writer.println(smClient.getString("managerServlet.findleaksFail"));
return;
}
String[] results =
((StandardHost) host).findReloadedContextMemoryLeaks();
if (results.length > 0) {
if (statusLine) {
writer.println(
smClient.getString("managerServlet.findleaksList"));
}
for (String result : results) {
if ("".equals(result)) {
result = "/";
}
writer.println(result);
}
} else if (statusLine) {
writer.println(smClient.getString("managerServlet.findleaksNone"));
}
}
protected void sslReload(PrintWriter writer, String tlsHostName, StringManager smClient) {
Connector connectors[] = getConnectors();
boolean found = false;
for (Connector connector : connectors) {
if (Boolean.TRUE.equals(connector.getProperty("SSLEnabled"))) {
ProtocolHandler protocol = connector.getProtocolHandler();
if (protocol instanceof AbstractHttp11Protocol>) {
AbstractHttp11Protocol> http11Protoocol = (AbstractHttp11Protocol>) protocol;
if (tlsHostName == null || tlsHostName.length() == 0) {
found = true;
http11Protoocol.reloadSslHostConfigs();
} else {
SSLHostConfig[] sslHostConfigs = http11Protoocol.findSslHostConfigs();
for (SSLHostConfig sslHostConfig : sslHostConfigs) {
if (sslHostConfig.getHostName().equalsIgnoreCase(tlsHostName)) {
found = true;
http11Protoocol.reloadSslHostConfig(tlsHostName);
}
}
}
}
}
}
if (found) {
if (tlsHostName == null || tlsHostName.length() == 0) {
writer.println(smClient.getString("managerServlet.sslReloadAll"));
} else {
writer.println(smClient.getString("managerServlet.sslReload", tlsHostName));
}
} else {
writer.println(smClient.getString("managerServlet.sslReloadFail"));
}
}
/**
* Write some VM info.
*
* @param writer The output writer
* @param smClient StringManager for the client's locale
* @param requestedLocales the client's locales
*/
protected void vmInfo(PrintWriter writer, StringManager smClient,
Enumerationnull to list resources of all types
* @param smClient i18n support for current client's locale
*/
protected void resources(PrintWriter writer, String type,
StringManager smClient) {
if (debug >= 1) {
if (type != null) {
log("resources: Listing resources of type " + type);
} else {
log("resources: Listing resources of all types");
}
}
// Is the global JNDI resources context available?
if (global == null) {
writer.println(smClient.getString("managerServlet.noGlobal"));
return;
}
// Enumerate the global JNDI resources of the requested type
if (type != null) {
writer.println(smClient.getString("managerServlet.resourcesType",
type));
} else {
writer.println(smClient.getString("managerServlet.resourcesAll"));
}
printResources(writer, "", global, type, smClient);
}
/**
* List the resources of the given context.
*
* @param writer Writer to render to
* @param prefix Path for recursion
* @param namingContext The naming context for lookups
* @param type Fully qualified class name of the resource type of interest,
* or null to list resources of all types
* @param clazz Unused
* @param smClient i18n support for current client's locale
*
* @deprecated Use {@link #printResources(PrintWriter, String,
* javax.naming.Context, String, StringManager)}
* This method will be removed in Tomcat 10.x onwards
*/
@Deprecated
protected void printResources(PrintWriter writer, String prefix,
javax.naming.Context namingContext,
String type, Class> clazz, StringManager smClient) {
printResources(writer, prefix, namingContext, type, smClient);
}
/**
* List the resources of the given context.
* @param writer Writer to render to
* @param prefix Path for recursion
* @param namingContext The naming context for lookups
* @param type Fully qualified class name of the resource type of interest,
* or null to list resources of all types
* @param smClient i18n support for current client's locale
*/
protected void printResources(PrintWriter writer, String prefix,
javax.naming.Context namingContext,
String type,
StringManager smClient) {
try {
NamingEnumerationtrue if a webapp with that name is deployed
* @throws Exception Propagate JMX invocation error
*/
protected boolean isDeployed(String name)
throws Exception {
String[] params = { name };
String[] signature = { "java.lang.String" };
Boolean result =
(Boolean) mBeanServer.invoke(oname, "isDeployed", params, signature);
return result.booleanValue();
}
/**
* Invoke the check method on the deployer.
*
* @param name The webapp name
* @throws Exception Propagate JMX invocation error
*/
protected void check(String name)
throws Exception {
String[] params = { name };
String[] signature = { "java.lang.String" };
mBeanServer.invoke(oname, "check", params, signature);
}
/**
* Invoke the isServiced method on the deployer.
*
* @param name The webapp name
* @return true if a webapp with that name is being serviced
* @throws Exception Propagate JMX invocation error
*/
protected boolean isServiced(String name)
throws Exception {
String[] params = { name };
String[] signature = { "java.lang.String" };
Boolean result =
(Boolean) mBeanServer.invoke(oname, "isServiced", params, signature);
return result.booleanValue();
}
/**
* Invoke the addServiced method on the deployer.
*
* @param name The webapp name
* @throws Exception Propagate JMX invocation error
*/
protected void addServiced(String name)
throws Exception {
String[] params = { name };
String[] signature = { "java.lang.String" };
mBeanServer.invoke(oname, "addServiced", params, signature);
}
/**
* Invoke the removeServiced method on the deployer.
*
* @param name The webapp name
* @throws Exception Propagate JMX invocation error
*/
protected void removeServiced(String name)
throws Exception {
String[] params = { name };
String[] signature = { "java.lang.String" };
mBeanServer.invoke(oname, "removeServiced", params, signature);
}
/**
* Delete the specified directory, including all of its contents and
* subdirectories recursively. The code assumes that the directory exists.
*
* @param dir File object representing the directory to be deleted.
* @return true if the deletion was successful
*/
protected boolean undeployDir(File dir) {
String files[] = dir.list();
if (files == null) {
files = new String[0];
}
for (int i = 0; i < files.length; i++) {
File file = new File(dir, files[i]);
if (file.isDirectory()) {
if (!undeployDir(file)) {
return false;
}
} else {
if (!file.delete()) {
return false;
}
}
}
return dir.delete();
}
/**
* Upload the WAR file included in this request, and store it at the
* specified file location.
*
* @param writer Writer to render to
* @param request The servlet request we are processing
* @param war The file into which we should store the uploaded WAR
* @param smClient The StringManager used to construct i18n messages based
* on the Locale of the client
*
* @exception IOException if an I/O error occurs during processing
*/
protected void uploadWar(PrintWriter writer, HttpServletRequest request,
File war, StringManager smClient) throws IOException {
if (war.exists() && !war.delete()) {
String msg = smClient.getString("managerServlet.deleteFail", war);
throw new IOException(msg);
}
try (ServletInputStream istream = request.getInputStream();
BufferedOutputStream ostream =
new BufferedOutputStream(new FileOutputStream(war), 1024)) {
byte buffer[] = new byte[1024];
while (true) {
int n = istream.read(buffer);
if (n < 0) {
break;
}
ostream.write(buffer, 0, n);
}
} catch (IOException e) {
if (war.exists() && !war.delete()) {
writer.println(
smClient.getString("managerServlet.deleteFail", war));
}
throw e;
}
}
protected static boolean validateContextName(ContextName cn,
PrintWriter writer, StringManager smClient) {
// ContextName should be non-null with a path that is empty or starts
// with /
if (cn != null &&
(cn.getPath().startsWith("/") || cn.getPath().equals(""))) {
return true;
}
String path = null;
if (cn != null) {
path = Escape.htmlElementContent(cn.getPath());
}
writer.println(smClient.getString("managerServlet.invalidPath", path));
return false;
}
/**
* Copy the specified file or directory to the destination.
*
* @param src File object representing the source
* @param dest File object representing the destination
* @return true if the copy was successful
*/
public static boolean copy(File src, File dest) {
boolean result = false;
try {
if( src != null &&
!src.getCanonicalPath().equals(dest.getCanonicalPath()) ) {
result = copyInternal(src, dest, new byte[4096]);
}
} catch (IOException e) {
e.printStackTrace();
}
return result;
}
/**
* Copy the specified file or directory to the destination.
*
* @param src File object representing the source
* @param dest File object representing the destination
* @param buf Temp byte buffer
* @return true if the copy was successful
*/
public static boolean copyInternal(File src, File dest, byte[] buf) {
boolean result = true;
String files[] = null;
if (src.isDirectory()) {
files = src.list();
result = dest.mkdir();
} else {
files = new String[1];
files[0] = "";
}
if (files == null) {
files = new String[0];
}
for (int i = 0; (i < files.length) && result; i++) {
File fileSrc = new File(src, files[i]);
File fileDest = new File(dest, files[i]);
if (fileSrc.isDirectory()) {
result = copyInternal(fileSrc, fileDest, buf);
} else {
try (FileInputStream is = new FileInputStream(fileSrc);
FileOutputStream os = new FileOutputStream(fileDest)){
int len = 0;
while (true) {
len = is.read(buf);
if (len == -1)
break;
os.write(buf, 0, len);
}
} catch (IOException e) {
e.printStackTrace();
result = false;
}
}
}
return result;
}
protected Map