This article was last updated: 2017-11-27 Personal blog address: blog.sqdyy.cn

introduce

When I was an intern at the Postal Service, the leader asked me to read the source code of Tomcat and try to implement the function of remote deployment project by myself, which led to this practice.

In Tomact, there is a Manager application, which is used to manage the deployed Web application. In this application, ManagerServlet is the main servlet, which can obtain part of tomcat metrics and remotely manage the Web application. However, this functionality is protected by security constraints in web application deployment.

When you request the ManagerServlet, it checks the value returned by getPathInfo() and the associated query parameters to determine the requested operation. It supports the following operations and parameters (starting with the servlet path):

Request path describe
/deploy? config={config-url} Deploy and start a new Web application according to the specified path (see source code)
/deploy? config={config-url}&war={war-url}/ Deploy and start a new Web application based on the specified PAT (see source code)
/deploy? path=/xxx&war={war-url} Deploy and start a new Web application according to the specified path (see source code)
/list Lists the context paths of all Web applications. Format: PATH: Status: Sessions (number of active sessions)
/reload? path=/xxx Reloads the Web application at the specified path
/resources? type=xxxx Enumerates available global JNDI resources to restrict the specified Java class name
/serverinfo Displays system information and JVM information
/sessions This method has expired
/expire? path=/xxx List session idle time of web applications in path
/expire? path=/xxx&idle=mm Expire sessions for the context path /xxx which were idle for at least mm minutes.
/sslConnectorCiphers Display the diagnostic information about the SSL/TLS password configured for the connector
/start? path=/xx Starts the Web application at the specified path
/stop? path=/xxx Closes the Web application at the specified path
/threaddump Write a JVM thread dump
/undeploy? path=/xxx Close and delete the Web application at the specified path, then delete the underlying WAR file or document base directory.

We can remotely deploy our projects to the server using the operations provided by getPathInfo() in the ManagerServlet. I’ll post my code for the practice below. To do this, you only need to introduce the HttpClient and Commons packages.

Encapsulates a unified remote request management class

Encapsulate this class to make it easy for a client to request a ManagerServlet:

import java.io.File;
import java.net.URL;
import java.net.URLEncoder;

import org.apache.commons.io.IOUtils;
import org.apache.commons.lang.StringUtils;
import org.apache.http.Header;
import org.apache.http.HttpHost;
import org.apache.http.HttpResponse;
import org.apache.http.HttpStatus;
import org.apache.http.auth.AuthScope;
import org.apache.http.auth.Credentials;
import org.apache.http.auth.UsernamePasswordCredentials;
import org.apache.http.client.AuthCache;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.client.methods.HttpRequestBase;
import org.apache.http.client.protocol.ClientContext;
import org.apache.http.impl.auth.BasicScheme;
import org.apache.http.impl.client.BasicAuthCache;
import org.apache.http.impl.client.DefaultHttpClient;
import org.apache.http.impl.conn.PoolingClientConnectionManager;
import org.apache.http.protocol.BasicHttpContext;

public class TomcatManager {
    private static final String MANAGER_CHARSET = "UTF-8";
    private String username;
    private URL url;
    private String password;
    private String charset;
    private boolean verbose;
    private DefaultHttpClient httpClient;
    private BasicHttpContext localContext;

    /** constructor */
    public TomcatManager(URL url, String username) {
        this(url, username, "");
    }
    public TomcatManager(URL url, String username, String password) {
        this(url, username, password, "ISO-8859-1");
    }
    public TomcatManager(URL url, String username, String password, String charset) {
        this(url, username, password, charset, true);
    }
    public TomcatManager(URL url, String username, String password, String charset, boolean verbose) {
        this.url = url;
        this.username = username;
        this.password = password;
        this.charset = charset;
        this.verbose = verbose;
        
        // Create the network request configuration
        PoolingClientConnectionManager poolingClientConnectionManager = new PoolingClientConnectionManager();
        poolingClientConnectionManager.setMaxTotal(5);
        this.httpClient = new DefaultHttpClient(poolingClientConnectionManager);

        if (StringUtils.isNotEmpty(username)) {
            Credentials creds = new UsernamePasswordCredentials(username, password);

            String host = url.getHost();
            int port = url.getPort() > -1 ? url.getPort() : AuthScope.ANY_PORT;
            httpClient.getCredentialsProvider().setCredentials(new AuthScope(host, port), creds);

            AuthCache authCache = new BasicAuthCache();
            BasicScheme basicAuth = new BasicScheme();
            HttpHost targetHost = new HttpHost(url.getHost(), url.getPort(), url.getProtocol());
            authCache.put(targetHost, basicAuth);

            localContext = newBasicHttpContext(); localContext.setAttribute(ClientContext.AUTH_CACHE, authCache); }}/** Deploy and start a new application with the specified path */
    public TomcatManagerResponse deploy(String path, File war, boolean update) throws Exception {
        StringBuilder buffer = new StringBuilder("/deploy");
        buffer.append("? path=").append(URLEncoder.encode(path, charset));
        if(war ! =null) {
            buffer.append("&war=").append(URLEncoder.encode(war.toString(), charset));
        }
        if (update) {
            buffer.append("&update=true");
        }
        return invoke(buffer.toString());
    }

    /** Get the context path for all deployed Web applications. The format is PATH: STATUS: Sessions (number of active sessions) */
    public TomcatManagerResponse list(a) throws Exception {
        StringBuilder buffer = new StringBuilder("/list");
        return invoke(buffer.toString());
    }

    /** Get system information and JVM information */
    public TomcatManagerResponse serverinfo(a) throws Exception {
        StringBuilder buffer = new StringBuilder("/serverinfo");
        return invoke(buffer.toString());
    }

    /** How to actually send the request */
    private TomcatManagerResponse invoke(String path) throws Exception {
        HttpRequestBase httpRequestBase = new HttpGet(url + path);
        HttpResponse response = httpClient.execute(httpRequestBase, localContext);

        int statusCode = response.getStatusLine().getStatusCode();
        switch (statusCode) {
            case HttpStatus.SC_OK: / / 200
            case HttpStatus.SC_CREATED: / / 201
            case HttpStatus.SC_ACCEPTED: / / 202
                break;
            case HttpStatus.SC_MOVED_PERMANENTLY: / / 301
            case HttpStatus.SC_MOVED_TEMPORARILY: / / 302
            case HttpStatus.SC_SEE_OTHER: / / 303
            String redirectUrl = getRedirectUrl(response);
            this.url = new URL(redirectUrl);
            return invoke(path);
        }

        return new TomcatManagerResponse().setStatusCode(response.getStatusLine().getStatusCode())
                .setReasonPhrase(response.getStatusLine().getReasonPhrase())
                .setHttpResponseBody(IOUtils.toString(response.getEntity().getContent()));
    }
    
    /** Extract the redirected URL */
    protected String getRedirectUrl(HttpResponse response) {
        Header locationHeader = response.getFirstHeader("Location");
        String locationField = locationHeader.getValue();
        // is it a relative Location or a full ?
        return locationField.startsWith("http")? locationField : url.toString() +'/'+ locationField; }}Copy the code

Encapsulate the response result set

@Data
public class TomcatManagerResponse {
    private int statusCode;
    private String reasonPhrase;
    private String httpResponseBody;
}
Copy the code

Testing remote deployment

Prior to testing, please enable the following user permissions in the configuration file:

<role rolename="admin-gui"/>
<role rolename="admin-script"/>
<role rolename="manager-gui"/>
<role rolename="manager-script"/>
<role rolename="manager-jmx"/>
<role rolename="manager-status"/>
<user username="sqdyy" password="123456" roles="manager-gui,manager-script,manager-jmx,manager-status,admin-script,admin-gui"/>
Copy the code

Here is the code to test a successful remote deployment of a WAR package:

import static org.testng.AssertJUnit.assertEquals;
import java.io.File;
import java.net.URL;
import org.testng.annotations.Test;

public class TestTomcatManager {

    @Test
    public void testDeploy(a) throws Exception {
        TomcatManager tm = new TomcatManager(new URL("http://localhost:8080/manager/text"), "sqdyy"."123456");
        File war = new File("E: \ \ tomcat \ \ simple - war - project - 1.0 - the SNAPSHOT. War");
        TomcatManagerResponse response = tm.deploy("/ simple - war - project - 1.0 - the SNAPSHOT", war, true);
        System.out.println(response.getHttpResponseBody());
        assertEquals(200, response.getStatusCode());
        
        // output:
        // Deployed ok-Deployed Application at context path/simpe-war-project-1.0-snapshot
    }

    @Test
    public void testList(a) throws Exception {
        TomcatManager tm = new TomcatManager(new URL("http://localhost:8080/manager/text"), "sqdyy"."123456");
        TomcatManagerResponse response = tm.list();
        System.out.println(response.getHttpResponseBody());
        assertEquals(200, response.getStatusCode());
        
        // output:
        // OK - Listed applications for virtual host localhost
        // /:running:0:ROOT
        / / / simple - war - project - 1.0 - the SNAPSHOT: running: 0: simple - war - project - 1.0 - the SNAPSHOT
        // /examples:running:0:examples
        // /host-manager:running:0:host-manager
        // /manager:running:0:manager
        // /docs:running:0:docs
    }

    @Test
    public void testServerinfo(a) throws Exception {
        TomcatManager tm = new TomcatManager(new URL("http://localhost:8080/manager/text"), "sqdyy"."123456");
        TomcatManagerResponse response = tm.serverinfo();
        System.out.println(response.getHttpResponseBody());
        assertEquals(200, response.getStatusCode());
        
        // output:
        // OK - Server info
        // Tomcat Version: Apache Tomcat/7.0.82
        // OS Name: Windows 10
        / / OS Version: 10.0
        // OS Architecture: amd64
        / / the JVM Version: 1.8.0 comes with _144 - b01
        // JVM Vendor: Oracle Corporation}}Copy the code

The resources

Source address of ManagerServlet