0 背景
7.25Tomcat设计模式☞反应器模式(终)_哔哩哔哩_bilibili
tomact9-markdown: tomcat9源码解读文档 - Gitee.com
本文架构图及类图均来自上述引用。
整理之前的学习笔记,学习的目的是为Tomcat回显和内存马做准备,重点关注response对象、listener相关操作和对象、valve相关操作和对象、filter相关操作和对象、servletfilter相关操作和对象的走向。
1 环境搭建
1.1 Tomcat源码部署
这里选用Tomcat版本8.5.98,源码地址:Release 8.5.98 · apache/tomcat · GitHub
Tomcat源码部署可参考:【源码系列】Tomcat环境搭建_哔哩哔哩_bilibili
(1)下载后解压项目,并创建一个pom.xml,将下面内容复制到pom.xml中
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0
http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>org.apache.tomcat</groupId>
<artifactId>apache-tomcat</artifactId>
<version>8.5.98</version>
<dependencies>
<dependency>
<groupId>org.apache.ant</groupId>
<artifactId>ant</artifactId>
<version>1.10.4</version>
</dependency>
<dependency>
<groupId>wsdl4j</groupId>
<artifactId>wsdl4j</artifactId>
<version>1.6.2</version>
</dependency>
<dependency>
<groupId>javax.xml</groupId>
<artifactId>jaxrpc-api</artifactId>
<version>1.1</version>
</dependency>
<dependency>
<groupId>org.eclipse.jdt.core.compiler</groupId>
<artifactId>ecj</artifactId>
<version>4.5.1</version>
</dependency>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.13</version>
</dependency>
</dependencies>
<build>
<finalName>apache-tomcat</finalName>
<sourceDirectory>java</sourceDirectory>
<resources>
<resource>
<directory>java</directory>
</resource>
</resources>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.8.1</version>
<configuration>
<source>1.8</source>
<target>1.8</target>
<encoding>UTF-8</encoding>
</configuration>
</plugin>
</plugins>
</build>
</project>
(2)IDEA导入项目,并加载pom.xml,变为Maven项目
(3)定位到org.apache.catalina.startup.Bootstrap启动类的main函数,启动运行。
(4)可能遇到的问题
中文乱码解决:修改如下代码
- org/apache/jasper/compiler/Localizer
- org/apache/tomcat/util/res/StringManager
JSP解析报错:修改org/apache/catalina/startup/ContextConfig类的代码
(5)项目基本目录架构
- bin文件夹:存放二进制文件,例如startup.bat和shutdown.bat分别用来启动和关闭Tomcat;
- conf文件夹:用来存放配置文件,如server.xml和web.xml分别存放Tomcat配置和Web应用配置;
- java文件夹:Tomcat源码;
- webapps文件夹:存放Web应用。
1.2 创建和部署Servlet项目
(1)创建一个Servlet项目
(2)创建自定义的Listener、Filter、Servlet,并完成web.xml配置
package com.example.servletdemo1;
import javax.servlet.ServletContextEvent;
import javax.servlet.ServletContextListener;
public class MyListenr implements ServletContextListener {
@Override
public void contextInitialized(ServletContextEvent sce) {
System.out.println("自定义监听器初始化");
// ServletContextListener.super.contextInitialized(sce); // 需要注释掉,否则报错
}
@Override
public void contextDestroyed(ServletContextEvent sce) {
// ServletContextListener.super.contextDestroyed(sce); // 需要注释掉,否则报错
}
}
package com.example.servletdemo1;
import javax.servlet.*;
import javax.servlet.annotation.WebFilter;
import java.io.IOException;
public class MyFilter implements Filter {
@Override
public void init(FilterConfig filterConfig) throws ServletException {
System.out.println("自定义过滤器初始化成功");
// Filter.super.init(filterConfig); // 需要注释掉,否则报错
}
@Override
public void destroy() {
Filter.super.destroy();
}
@Override
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
System.out.println("hello Filter");
filterChain.doFilter(servletRequest, servletResponse);
}
}
package com.example.servletdemo1;
import java.io.*;
import javax.servlet.http.*;
import javax.servlet.annotation.*;
//@WebServlet(name = "helloServlet", value = "/hello-servlet")
public class HelloServlet extends HttpServlet {
private String message;
public void init() {
message = "Hello World!";
}
public void doGet(HttpServletRequest request, HttpServletResponse response) throws IOException {
response.setContentType("text/html");
// Hello
PrintWriter out = response.getWriter();
out.println("<html><body>");
out.println("<h1>" + message + "</h1>");
out.println("</body></html>");
}
public void destroy() {
}
}
(3)将项目部署到对应的Tomcat上,用IDEA生成exploded项目
(4)将打部署好项目放到Tomcat的webapps文件夹下,改一下名字,并将其他应用删掉(方便后面调试)。
2 Tomcat基本架构
2.1 总体架构
Tomcat设计了两个核心组件:连接器(Connect)和容器(Container)。Server表示着整个服务器。Server下面有多个服务Service,每个服务都包含着多个连接器组件Connector(Coyote实现)和一个容器组件Container。
Cononect负责socket通信,以及将原始的request/response转换为Servletrequest/Servletresponse,供Container使用。Container分为四层Engine(唯一)、Host(多个)、Context(多个)、Wrapper(多个),其中Host就是主机/IP/域名等,Context的集体的web项目,Wrapper是具体Servlet类的封装。Tomcat Container通过Pipeline-Valve责任链的形式,来处理servlet请求,Pipeline负责管理Valve,Valve是Container容器中真正处理请求逻辑的地方,每个Container都有自己的pipeline来关自己层级的Valve。
Tomcat架构部分的相关实现类如下。
2.2 Tomcat生命周期——Lifecycle接口
Tomcat的组件生命周期通过Lifecycle接口提供相应的方法来管理组件的生命周期(init、start、stop、destroy...),提供相应的事件常量来调用监听器处理不同的生命周期状态事件。这里主要采用了模板设计模式、观察者模式和状态模式。
xxxBase实现类来提供模板,LifecycleState类定义了状态和事件的关系,LifecycleListener是监听者,LifecycleEvent是监听者准备执行的事件。
此外LifecycleMBeanBase继承了LifecycleBase,主要负责JMX方面的内容,用于管理和操作 MBean,MBean通过暴露一组属性、操作和通知,允许外部程序监控和管理应用程序或系统的状态和行为。
而ValveBase、StandardServer、StandardService、Connector、ContaierBase又继承了LifecycleMBeanBase,这些都Tomcat中比较核心的组件或组件基类,这里通过继承来实现组件的生命周期管理。
2.2.1 LifecycleState类
LifecycleState类定义了状态和事件的关系,每一个状态都对应一个生命周期事件名称,即状态变了,事件也会相应的进行切换,而不同的事件会触发生命周期组件相应的监听器的执行。
2.2.2 LifecycleListener
生命周期监听器接口,内部就一个方法,主要就是基于特定的生命周期事件进行业务处理。监听器是启动过程中,真正干活的地方,根据不同的生命周期状态,进行相应处理,例如去加载各种配置。常见的监听器包括:MapperListener、EngineConfig、HostConfig、ContextConfig。
2.2.2.1 MapperListener
映射器监听器,其本身也是一个生命周期组件,同时又实现了LifecycleListener和ContainerListener两个监听器接口,ContainerListener属于容器生命周期组件特有的,说明MapperListener可以响应container的事件,监听tomcat组件的变化,当有Host,Context及Wrapper变更时,调用Mapper类中的相关方法,增加或者删除Host,Context,Wrapper等。
Tomcat就是依赖Mapper类来维持请求路径到具体Servlet处理类之间的映射关系,使得Tomcat能够正确处理请求。
2.2.2.2 EngineConfig
在Tomcat中,Engine是第一个容器组件,主要起到承上启下的作用,在Tomcat启动的处理流程中并没有出力。
2.2.2.3 HostConfig
Host接口是一个虚拟主机组件,相当于域名或IP,StandardHost是Host的具体实现类。HostConfig为StandardHost容器组件生命周期监听器(非容器监听器)中的一员,要作用就是部署我们的应用(也称作上下文)。
2.2.2.4 ContextConfig
Context就是具体的一个应用,在Tomcat中webapps文件下的一个每个项目就是一个Context,它回去解析应用的web.xml,在mapper中创建对应的servlet映射,管理Filter的处理逻辑。在Tomcat内存马中,重点就是要拿到项目的StandardContext(Context接口的标准实现类)。
2.2.3 LifecycleEvent
生命周期事件类,扩展了JDK的EventObject,增加了两个成员变量type和data。data一般为空,type就是发生的事件,例如Lifecycle中的事件常量,此外在Host接口、Context接口中也有相应的事件常量,而HostConfig、ContextConfig。这个类更多是起到标识的作用。
2.2.4 LifecycleBase
生命周期基类,一般以"Base"结尾的大部分都是抽象型的,而我们在面向对象的世界里定义抽象类,大多数是想定义一些抽象的方法让子类去实现(比如initInternal、startInternal、stopInternal、destroyInternal),除了交给子类实现的abstract methods外,抽象类中还会定义这一类型都共有的成员变量(比如listeners和state)以及执行步骤固定的业务方法(比如init、start、stop和destroy),大多数抽象类会应用设计模式中的【模板方法】,可以说只要是框架中的抽象类,无一例外都会用到。其变量和方法如下:
2.2.5 LifecycleMBeanBase
LifecycleMBeanBase继承了LifecycleBase,主要负责JMX方面的内容,向MBeanServer注册和卸载Bean ,MBean通过暴露一组属性、操作和通知,允许外部程序监控和管理应用程序或系统的状态和行为。
2.2.6 例子server.init()
下面就以Server实例(server)的init()方法调用过程来说明上述组件是如何工作的。Server是一个接口,具体的实现类是StandardServer,但StandardServer并没有实现init(),所以在调用server.init()的时候,会去调用间接父类LifecycleBase.init(),如下:这里需要关注两行代码。
2.2.4.1 setStateInternal函数
这里其实就是的代码执行流程:根据不同的生命周期状态,调用监听器的相应的状态事件,这里就是体现状态模式和监听器模式的地方。
跟进fireLifecycleEvent函数,就会发现这里会调用相关的监听器执行事件。
2.2.4.2 initInternal函数
在LifecycleBase类中initInternal函数是一个抽象函数,具体的实现方法由具体子类实现。而StandardServer便是实现了initInternal函数。这里体现了模板设计模式的思想,父类LifecycleBase提供init的模板,定义工作流程,子类StandardServer自己实现其中的某些抽象函数,实现自己的业务逻辑。不仅init(),其他的生命周期状态函数也是用了模板模式。
顺便一提:StandardServer.init()会for循环调用service.init()。
2.3 Connector
在Tomcat中,Connector组件被称为Coyote。这里主要采用了反应器模式来拓展Tomcat的连接池,使用命令模式来处理请求。
2.3.1 Coyote连接器架构
每一个Service中可以包括多个Connector,Connector中有两个比较重要的变量:ProtocolHander和Adapter,ProtocolHander主要负责socket通信(Endpoint)、协议处理生产request/response(Processor)和将原始的request/response适配为适合Servlet容器使用的ServletRequest/ServletResponse(Adapter)。
组件 | 描述 |
---|---|
ProtocolHandler | Coyote 协议接口,通过Endpoint和Processor,实现针对具体协议的处理能力。Tomcat按照协议和I/O提供了6个实现类:AjpNioProtocol,AjpAprProtocol,AjpNio2Protocol , Http11NioProtocol , Http11Nio2Protocol ,Http11AprProtocol |
Processor | 用于将Endpoint接收到的socket处理封装成request对象,实现HTTP协议。 |
Endpoint | 通信端点,处理底层socket网络连接,实现TCP/IP协议。 |
Acceptor | 循环监听和限制客户端请求,客户端请求超过最大连接数,就阻塞,否则将socket交给Poller处理,属于守护线程。 |
Poller | 工作线程,将Acceptor扔过来的客户端socketChannel进行事件注册,注册后的事件称作PollerEvent,保存到队列中,poller内部维护一个selector,poller线程循环事件队列,并通过selector监听客户端通道IO读写事件,这个过程属于同步非阻塞,效率非常高,如果IO读写就绪,就将当前的channel交给SocketProcessor任务,该任务最终会扔给Tomcat扩展JDK的线程池来处理。同Acceptor,该线程也是一个守护线程。 |
SocketProcessor | 处理有IO读写事件发生的客户端channel,然后封装一番交给对应的protocolHandler进行处理。 |
ThreadPoolExecutor | 处理客户端请求的线程。 |
Adapter | 由于协议不同,客户端发过来的请求信息也不尽相同,Tomcat定义了自己的Request 类来封装这些请求信息。ProtocolHandler接口负责解析请求并生成Tomcat Request类。但是这个Request对象不是标准的ServletRequest,不能被CoyoteAdapter使用,这是设配器模式的经典运用,连接器调用CoyoteAdapter的Service方法,传入的是Tomcat Request对象,CoyoteAdapter负责将这个对象转换成ServletRequest,再传递给容器。 |
Connector类的比较核心成员如下:
public class Connector extends LifecycleMBeanBase {
/**
* The <code>Service</code> we are associated with (if any).
* 当前connector连接器关联的service,一个service有多个connector
*/
protected Service service = null;
/**
* Coyote protocol handler. coyote 连接器组件协议处理器
*/
protected final ProtocolHandler protocolHandler;
/**
* Maximum size of a POST which will be automatically parsed by the
* 容器表单 URL 参数解析将处理的 POST 的最大大小(以字节为单位)。通过将此属性设置为小于零的值,
* 可以禁用该限制。如果未指定,则此属性设置为 2097152(2 MB)。请注意,FailedRequestFilter 可用于拒绝超过此限制的请求。
*/
protected int maxPostSize = 2 * 1024 * 1024;
/**
* Coyote adapter.
* coyote适配器,负责将前端的request请求转换成HttpServletRequest转交给container(就是servlet应用,后端服务)
*/
protected Adapter adapter = null;
public Connector() {
/**默认协议处理器 {@link org.apache.coyote.http11.Http11NioProtocol}*/
this("HTTP/1.1");
}
public Connector(String protocol) {
boolean apr = AprStatus.getUseAprConnector() && AprStatus.isInstanceCreated()
&& AprLifecycleListener.isAprAvailable();
ProtocolHandler p = null;
try {
/**基于协议类型和是否开启apr来创建协议处理器*/
p = ProtocolHandler.create(protocol, apr);
} catch (Exception e) {
log.error(sm.getString(
"coyoteConnector.protocolHandlerInstantiationFailed"), e);
}
if (p != null) {
protocolHandler = p;
protocolHandlerClassName = protocolHandler.getClass().getName();
} else {
protocolHandler = null;
protocolHandlerClassName = protocol;
}
// Default for Connector depends on this system property
setThrowOnFailure(Boolean.getBoolean("org.apache.catalina.startup.EXIT_ON_INIT_FAILURE"));
}
/**以反射的方式设置protocolHandler的各个属性值,主要就是优化线程池相关的配置*/
public boolean setProperty(String name, String value) {
if (protocolHandler == null) {
return false;
}
return IntrospectionUtils.setProperty(protocolHandler, name, value);
}
}
ProtocolHandler:AbstractProtocol实现类声明了endpoint变量,实现了process方法,在process方法中,会创建proceessor实例,来处理请求。
Endpoint相关类:HTTP/1.1 对应的TCP节点默认是NioEndpoint。
这里看AbstractEndpoint和NioEndpoint类的成员可以知道,AbstractEndpoint会关联一个endpoint,在接收到socket连接后,会到相应的endpoint线程(Poller)中,NioEndpoint这些子类自己实现了一个内部类Poller。
Processor类:初步处理请求的地方,前面几个类是用来解决socket连接线程的。
Adapter:CoyoteAdapter是Adapter接口的唯一实现类,这里采用了是适配器模式,通过CoyoteAdapter.service来转换request/response使得容器能处理请求。
2.3.2 Tomcat中的命令模式
Tomcat中使用命令模式来处理请求。命令模式的类图如下:
- Invoker是一个命令控制器,里面通过setCommand可以聚合具体的命令(中间类,解耦了,增加的command的时候不需要改变Invoker)
- CommandImpl类:是不同的命令类,内部关联了具体的命令执行处理类
- Receiver:是命令执行处理类,是一个命令真正作用的对象。
Tomcat的请求处理流程如下:
1、Tomcat服务启动后,监听指定端口,客户端随后发送请求连接指定端口并附带传输文本数据;
2、Acceptor线程负责接收来自客户端的连接socket,然后交给具体的endpoint进行处理;
3、XXXEndpoint使用Poller线程对接收进来的socketChannel进行事件注册,同时轮询事件列表,如果有通道发生IO读事件,就交给指定的SocketProcessor线程进行处理;
4、SocketProcessor线程最终会调用对应的Handler去处理socketWrapper;
5、Handler定义是在AbstractEndpoint类中,但是真正的实现类ConnectionHandler却是在AbstractProtocol中,最终Handler会调用对应的应用层XXXProcessor去处理套接字输入/出流,转换成Coyote框架中定义的request和response对象;
6、但这远远不够,因为容器需要接收来自servlet规范中的request和response类型,所以需要对coyote框架中的request和response进行一道转换,转换的目的就是为了能让coyote框架可以轻松的与Container容器建立连接,这个转换器(适配器)正是CoyoteAdapter;7、那么命令模式在哪体现呢? client端发送连接请求,Invoker调用者其实就是我们的Acceptor,Command就是我们的AbstractEndpoint,Acceptor持有一个endPoint,主要对客户端socket连接进行并发限制和IO读事件的轮询,然后转交给具体的Command(Apr,NIO,NIO2)内部的SocketProcessor进行doRun(),那么接收者是谁呢?那必然是ProtocolHandler啊,当然接收者也是有很多类型的,也就是一个endpoint反向关联着一个protocol,那么endpoint是如何将命令(socketWrapper)传递给protocol的呢(具体其实是特定协议对应的Processor),是通过Handler具体点就是ConnectionHandler传递过去的,因为Handler(连接处理)接口定义在AbstractEndpoint中,而实现却是在AbstractProtocol中,这就像牵线搭桥一样,将protocol与endpoint串联在了一起!
8、那么接收者Protocol#Processor做了什么事情呢?当然是对传递过来的socketWrapper命令进行解析了,最终将tcp层的socket输入输出流转换成应用层的request和response对象。
2.3.3 Tomcat中的反应器模式
为更好Tomcat在Connector中的设计与实现,可以先了解下Java的NIO和反应器模式: https://so.csdn.net/so/search?q=NIO&t=blog&u=cj151525
NIO就是同步非阻塞I/O模型。非阻塞主要体现在socket的接收/读/写函数上,在等待I/O就绪阶段都是非阻塞的,主要通过I/O多路复用技术来实现。NIO有三大核心组件:
组件名称 | 描述 | 核心类 |
---|---|---|
Buffer缓冲区 | 在Java NIO中,所有数据都是通过缓冲区对象进行传输的。缓冲区是一段连续的内存块,可以保存需要读写的数据。缓冲区对象包含了一些状态变量,例如容量(capacity)、限制(limit)、位置(position)等,用于控制数据的读写。 | ByteBuffer |
Channel通道 | 通道是一个用于读写数据的对象,类似于Java IO中的流(Stream)。与流不同的是,通道可以进行非阻塞式的读写操作,并且可以同时进行读写操作。通道分为两种类型:FileChannel和SocketChannel,分别用于文件和网络 | ServerSocketChannel/ SocketChannel/ FileChannel |
Selector选择器 | 选择器是Java NIO中的一个重要组件,它可以用于同时监控多个通道的读写事件,并在有事件发生时立即做出响应。选择器可以实现单线程监听多个通道的效果,从而提高系统吞吐量和运行效率。Selector又称作I/O多路复用器。 | Selector |
Tomcat采用了一种主从Reactor+线程池的请求连接处理模式,Acceptor线程负责接收socket请求,Poller线程(从Reatcor)作为一个Selector去遍历事件,如有相应IO事件
2.4 Container
2.4.1 Container
Tomcat中的Container具有层级结构,但不是通过继承来实现,而是通过parent(父容器)和child(子容器)变量来关联到下一层级。
ContainerBase类:模板类,里面会关联一个pipeline。
Engine接口和StandardEngine实现类:一个Service中只有一个Engine,Engine在Connector和Container中起到承上启下的作用,但其实没什么功能,相当于Container的入口吧。
Host和StandardHost:Host接口内定义了一些监听事件,由HostConifg类处理;StandardHost是主机,默认配置中仅一个,即localhost,但可以配置多个,主要负责Web应用的部署和Context应用上下文的创建。
Context和StandardContext:Context接口内定义了一些监听事件,由ContextConifg类处理;StandardContext为Web应用上下文,包含多个Wrapper,负责Web配置的解析、管理整个Web相关的资源,例如应用监听器的添加、Servlet的映射,包括。Tomcat的webapps目录下的一个目录,就是一个Web应用,也即是一个StandardContext,内存马就是要找到相应Web项目的StandardContext。
Wrapper和StandardWrapper:Wrapper作为最底层的容器,是对Servlet的封装,负责Servlet实例的创建、执行和销毁。
需要注意的是,每个service中会有一个mapper(这个mapper会传递到Engine等容器中)来保存各个容器的映射信息,确保请求能正确走到对应的Servlet中。
2.4.2 Pipeline-Valve
Tomcat采用Valve来干活,每个容器都有自己的pipeline(继承于父类),pipeline用来管理管道中Valve的调用链过程,通过调用invoke方法实现链式调用。其实Filter和Valve的工作模式是一样的,Filter是调用doFilter方法。
3 Tomcat启动流程
Tomcat的启动入口在org/apache/catalina/startup/Bootstrap.java的main函数中。这里主要有三个需要关注的点,重点在于后面两个函数的调用流程。daemon变量是Bootstrap实例。
- bootstrap.init():创建Catalina实例,设置相关类加载器;
- daemon.load():各个组件的初始化init()。
- daemon.start():各个组件的启动start()。
3.1 bootstrap.init();
这里只是创建Catalina实例和设置几个类加载。
3.2 daemon.load(args);
顺便一提:daemon.setAwait(true);将awaite标志位设置位true,后面Connector才能正常的监听。
daemon.load(args);会反射调用Catalina实例的load方法。
catalina.load()方法里面,有三个比较终于的点:
3.2.1 Digester digester = createStartDigester()和digester.parse(inputSource)
创建一个digester解析器,并用来解析Tomcat服务器配置文件server.xml,这里创建各个组件的实例,包括各种组件和监听器;
3.2.2 getServer().init()
获取当前Tomcat的server实例,并执行server.init()方法。
public void load() {
if (loaded) {
return;
}
loaded = true;
long t1 = System.nanoTime();
initDirs();
// Before digester - it may be needed
initNaming();
// Create and execute our Digester
Digester digester = createStartDigester(); //解析server.xml
InputSource inputSource = null;
InputStream inputStream = null;
File file = null;
try {
try {
file = configFile();
inputStream = new FileInputStream(file);
inputSource = new InputSource(file.toURI().toURL().toString());
} catch (Exception e) {
if (log.isDebugEnabled()) {
log.debug(sm.getString("catalina.configFail", file), e);
}
}
if (inputStream == null) {
try {
inputStream = getClass().getClassLoader()
.getResourceAsStream(getConfigFile());
inputSource = new InputSource
(getClass().getClassLoader()
.getResource(getConfigFile()).toString());
} catch (Exception e) {
if (log.isDebugEnabled()) {
log.debug(sm.getString("catalina.configFail",
getConfigFile()), e);
}
}
}
// This should be included in catalina.jar
// Alternative: don't bother with xml, just create it manually.
if (inputStream == null) {
try {
inputStream = getClass().getClassLoader()
.getResourceAsStream("server-embed.xml");
inputSource = new InputSource
(getClass().getClassLoader()
.getResource("server-embed.xml").toString());
} catch (Exception e) {
if (log.isDebugEnabled()) {
log.debug(sm.getString("catalina.configFail",
"server-embed.xml"), e);
}
}
}
if (inputStream == null || inputSource == null) {
if (file == null) {
log.warn(sm.getString("catalina.configFail",
getConfigFile() + "] or [server-embed.xml]"));
} else {
log.warn(sm.getString("catalina.configFail",
file.getAbsolutePath()));
if (file.exists() && !file.canRead()) {
log.warn("Permissions incorrect, read permission is not allowed on the file.");
}
}
return;
}
try {
inputSource.setByteStream(inputStream);
digester.push(this);
digester.parse(inputSource);
} catch (SAXParseException spe) {
log.warn("Catalina.start using " + getConfigFile() + ": " +
spe.getMessage());
return;
} catch (Exception e) {
log.warn("Catalina.start using " + getConfigFile() + ": " , e);
return;
}
} finally {
if (inputStream != null) {
try {
inputStream.close();
} catch (IOException e) {
// Ignore
}
}
}
getServer().setCatalina(this);
getServer().setCatalinaHome(Bootstrap.getCatalinaHomeFile());
getServer().setCatalinaBase(Bootstrap.getCatalinaBaseFile());
// Stream redirection
initStreams();
// Start the new server
try {
getServer().init();
} catch (LifecycleException e) {
if (Boolean.getBoolean("org.apache.catalina.startup.EXIT_ON_INIT_FAILURE")) {
throw new Error(e);
} else {
log.error("Catalina.start", e);
}
}
long t2 = System.nanoTime();
if(log.isInfoEnabled()) {
log.info("Initialization processed in " + ((t2 - t1) / 1000000) + " ms");
}
}
getServer().init():实际中是调用了LifecycleBase父类的init模板,调用init过程的监听器执行INITIALIZING状态事件,然后走到StandardServer.initInternal。(这里的监听器和内存马中用到的不相关,所以具体的执行流程就不写了;从下面代码可以知道,init()中除了执行监听器事件外,就是执行具体子类的initInternal,所以后面的书写中,init()和initInternal()不做区分。)
StandardServer.initInternal的代码如下:
父类LifecycleMBeanBase负责JMX方面的初始化工作。
3.2.3 getServer().init()调用了service.init():
StandardService类实现了Serive接口,StandardService.initInternal代码如下:
(1)engine.init():会配置安全访问模块(Realm)和调用父类容器ContainerBase,去创建单个线程的线程池,这里并不会去初始化host;
(2)mapperListenr.init():mapperListenr没有实现initInternal方法,这里会调用LifecycleMBeanBase.initInterna来注册MBean;
(3)connector.init():
跟进去看protocolHandler.init()->AbstractHttp11Protocol.init()->AbstractProtocol.init()->endpoint.init()。
endpoint是NioEndpoint实例,其init()是继承自父类AbstractJsseEndpoint的。跟进NioEndpoint.init()->AbstractJsseEndpoint.init()->AbstractEndpoint.init()->bind()。
bind()方法是AbstractEndpoint类中的一个抽象方法,在这里AbstractEndpoint是一个模板,核心bind由子类自己实现。跟进NioEndpoint.bind()
3.3 daemon.start()
daemon.start()用来启动组件。会反射调用Catalina实例的start()方法。
Catalina的start()方法代码如下:跟进,发现又回到了模板模式。server.start()->LifecycleBase.start()->server.startInternal()。
查看server.startInternal()代码:循环调用service.start()
跟进StandardService.start():主要是三部分的工作engine.start()、mapperListner.start()、connector.start()
3.3.1 engine.start()
作为容器启动入口,负责启动各容器,部署web上下文、关联servlet类。从engine开始,不仅需要关注自己实现的startInternal(),也要关注LifecycleBase.setStateInternal,因为监听器xxxConfig在启动流程会做事件处理。但engine只是承上启下的作用,EngineConfig并没有真正干活。这里只看startInternal()方法。
这里会调用直接父类Container.startInternal()的方法
把Callbale类型的StartChild线程加入线程池后,会另起线程异步调用StartChild.call()方法,call中会调用child.start()。在start启动过程中,真正起到作用的是LifecycleBase.start()中调用的setStateInternal,通过监听器来进行操作,最终操作点是listener.lifecycleEvent(event)。另外有一点需要注意的是,在上一节初始化的过程中,除了Engine外的其他容器都没有初始化,所以在start容器的过程中会先初始化(init)容器。这里的调试过程比较复杂,能力有限,这里只关注和内存马相关的内容。
首先是webConfig()函数,其中有两个比较重要的地方,一个是webXmlParser.parseWebXml(contextWebXml, webXml, false):负责解析web.xml,并赋值给webXML实例。
另一个是configureContext(webXml):将webXML实例的内容往Container中添加。
添加Servlet的步骤为:
for (ServletDef servlet : webxml.getServlets().values()) {
Wrapper wrapper = context.createWrapper();
// Description is ignored
// Display name is ignored
// Icons are ignored
// jsp-file gets passed to the JSP Servlet as an init-param
if (servlet.getLoadOnStartup() != null) {
wrapper.setLoadOnStartup(servlet.getLoadOnStartup().intValue());
}
if (servlet.getEnabled() != null) {
wrapper.setEnabled(servlet.getEnabled().booleanValue());
}
wrapper.setName(servlet.getServletName());
Map<String,String> params = servlet.getParameterMap();
for (Entry<String, String> entry : params.entrySet()) {
wrapper.addInitParameter(entry.getKey(), entry.getValue());
}
wrapper.setRunAs(servlet.getRunAs());
Set<SecurityRoleRef> roleRefs = servlet.getSecurityRoleRefs();
for (SecurityRoleRef roleRef : roleRefs) {
wrapper.addSecurityReference(
roleRef.getName(), roleRef.getLink());
}
wrapper.setServletClass(servlet.getServletClass());
MultipartDef multipartdef = servlet.getMultipartDef();
if (multipartdef != null) {
long maxFileSize = -1;
long maxRequestSize = -1;
int fileSizeThreshold = 0;
if(null != multipartdef.getMaxFileSize()) {
maxFileSize = Long.parseLong(multipartdef.getMaxFileSize());
}
if(null != multipartdef.getMaxRequestSize()) {
maxRequestSize = Long.parseLong(multipartdef.getMaxRequestSize());
}
if(null != multipartdef.getFileSizeThreshold()) {
fileSizeThreshold = Integer.parseInt(multipartdef.getFileSizeThreshold());
}
wrapper.setMultipartConfigElement(new MultipartConfigElement(
multipartdef.getLocation(),
maxFileSize,
maxRequestSize,
fileSizeThreshold));
}
if (servlet.getAsyncSupported() != null) {
wrapper.setAsyncSupported(
servlet.getAsyncSupported().booleanValue());
}
wrapper.setOverridable(servlet.isOverridable());
context.addChild(wrapper);
}
for (Entry<String, String> entry :
webxml.getServletMappings().entrySet()) {
context.addServletMappingDecoded(entry.getKey(), entry.getValue());
}
Tomcat中有两个默认的Servlet。
跟踪helloServlet,发现添加Servlet的时候的重要步骤有:
StandardContext.servlet.setLoadOnStartup:默认的StandardWrapper的loadOnStartup是-1,ServletDef的默认是null,随意自定义的helloServletloadOnStartup会保持默认值-1。
wrapper.setServletClass(servlet.getServletClass()):设置全限定类名,全限定名一般都是用来类加载的。
context.addChild(wrapper):将wrapper加入context中
然后添加servlet类和url映射关系
下一个重要的函数是loadServlet,调用链如下:
这里直接定位Standard.startInternal(),调试是如何加载Servlet的。顺便一提,Listener、Filter、Servlet都是从这里开始load的。前面在说如何分析的时候是利用处分析如何写内存马的,其实从Standard.startInternal()分析也行。
- listenerStart():
这里重点看listener是如何调用的
- filterStart():
- loadOnstartup:
这里有两个for循环:第一个是判断loadOnStartup是否大于0,大于就添加到map中,与自定义Servlet相关的wrapper类loadOnStartup值保存默认值为-1,所以不会添加到map中。
第二个循环就是从map中取值,进行wrapper.load()->loadServlet。loadServlet其实就是通过wrapper的servletClass值来进行serlvet实例的类加载,并把servlet实例赋给wrapper.instance变量。
3.3.2 mappperListener.start()
这里重点是将部署好的web项目,映射到mapper中,以便后续做请求处理。
3.3.3 connector.start()
开启acceptor和poller线程。connector.start()->connector.startInternal->protocolHandler.start()->(父类)AbstractProtocol.start()->endpoint.start()->Nioendpoint.startInternal()
4 Tomcat请求流程
4.1 acceptor线程
接收socket请求,并交给具体endpoint的poller线程处理先在socket = endpoint.serverSocketAccept()打断点,然后用浏览器发起请求。
调用Nioendpoint.serverSocketAccept()方法,等到socket连接,接收后,会返回对应的channe调用endpoint.setSocketOptions(socket)->poller.register(socketWrapper)->Nioendpoint.addEvent():将socket封装为socketWrapper,并将其添加到Poller的PollerEvent队列中,然后唤醒poller。
然后acceptor线程的工作完成,可以把acceptor线程相关端点去掉。
4.2 Poller线程
在AbstractEndpoint.processSocket()方法打个断点。重新调试发请求,可以看到调用栈:
NioEndpoint$Poller.run():轮询,等待IO事件,然后调用NioEndpoint.processKey处理。
NioEndpoint.processKey(sk, socketWrapper):主要一些条件判断,然后会调用processSocket来处理,processSocket方法的实现是来自于父类AbstractEndpoint。
AbstractEndpoint.processSocket:获取endpoint对应的处理器,并把处理线程放入worker线程队列中去。
4.3 Worker线程
可以在NioEndpoint$SocketProcessor.doRun()打断点,需要把其他线程的断点去掉。这里是通过调用worker线程组去处理请求,请求后续的操作都会在这个线程中完成。
直接到NioEndpoint$SocketProcessor.doRun,前头的调用栈只是启动worker中的SocketProcessBase线程,跑到doRun中去。
NioEndpoint$SocketProcessor.doRun->AbstractProtocol.process()
AbstractProtocol.process()->AbstractProcessorLight.process() :Http11Processor调用父类的process()
AbstractProcessorLight.process()->Http11Processor.service()
Http11Processor.service()解析request,并调用适配器的service,即getAdapter().service(request, response)
CoyoteAdapter.service开始调用容器管道的Valve来处理包装后的请求。那服务端是如何获取Valve的呢?知道获取流程就可以知道怎么和在哪添加恶意Valve。
从这段代码可以知道,response在request内,并且catalina的request会备份一个coyoute的request。从回显的角度来说,每个阶段的response可看作一致的,因为相互包含,所以只需要获得任一response,就能把消息传回前端。
4.3.1 Valve的调用流程
查看Valve的调用栈:这里选择StandardContext的两个Valve来说明执行流程:
首先是AuthenticatorBase,继承于ValveBase,每个Valve都有一个内部变量next,指向下一个Valve,通过getNext()获取下一个Valve实例,然后调用invoke方法。
然后是StandardContextValve,catalina的request中有一个mappingData用来存储各种容器,通过容器的管道去获取下一层级容器的第一个Valve,并调用Valve.invoke()。
容器会从父类ContainerBase中继承一个StandardPipeline来管理Valve。
StandardPipeline提供了addPipeline来添加Valve
所以如果想要添加自己的恶意Valve,需要完成如下动作:
1. 获取StandardContext(所有容器都可以,因为可以通过parent和child来获取别的容器)
2. 写一个Valve恶意类,继承ValveBase类,重写iinvoke方法,并在里面写入恶意代码
3. 通过StandardContext获取其pipeline,然后调用pipeline.addValve(Valve)将恶意类进去
4. 往后每次请求到来,就都会走到恶意Valve中执行恶意代码。
StandardWrapperValve.invoke():StandardWrapperValve是容器最后一个工作Valve,这里会主要完成两项工作:调用Filter链和调用servlet。
4.3.2 Filter的调用流程
先创建filterChain,然后调用filetrChain.doFilter()。
ApplicationFilterFactory.createFilterChain(request, wrapper, servlet):从Context中获取filterMaps:存放了Filter名称和urlPatterns。
ApplicationFilterFactory.createFilterChain(request, wrapper, servlet):接着遍历filerMaps,将filterConfig添加到addFilter里
filterChain.doFilter(request.getRequest(), response.getResponse());会跳到ApplicationFilterChain中,调用filterConfig.getFilter()
filterConfig.getFilter:先看filter是否存在,不存在就去filterDef中动态加载,所以在写内存马的时候,可以把filter套进filterDef中,避免filter不存在的情况。
最终会走到自定义的FIlter的doFilter方法中。
上面流程中用到的filterConfigs、filterDef、filterMaps都是StandardContext的变量获取,StandardContext还提供了对应addxx方法来添加filter 相关信息。
所以,写Tomcat的filter内存马可以通过以下步骤:在StandardContext中的FilterDefs、FilterMaps、filterConfigs中存放我们filter的内容。
1. 获取StandardContext
2. 创建一个恶意filter,在doFilter中写入恶意代码
3. 实例化一个FilterDef类,包装filter并存放到StandardContext.filterDefs中
4. 实例化一个FilterMap类,将恶意 Filter 和 urlpattern 相对应,存放到StandardContext.filterMaps中(一般会放在首位)
5. 通过反射获取StandardContext的filterConfigs,实例化一个filterConfig(ApplicationFilterConfig)类,传入StandardContext与filterDef,存放到filterConfigs中
4.3.3 Servlet的调用流程
ApplicationFilterChain.internalDoFilter,直接调用了servlet.service,ApplicationFilterChain有servlet变量。通过servlet.service()会调用对应的Servlet类代码。
调到这里发现有些步骤漏了,那就是ApplicationFilterChain的servlet变量是什么时候赋值的呢?ApplicationFilterChain有setServlet方法,在这里打个端点,重新调试。
ApplicationFilterFacotry.createFilterChain()调用了setServlet():发现在servlet变量是传过来的
继续往上,回到StandardWrapperValve.invoke(),发现servlet是通过wrapper.allocate获取。
由于自定义的wrapper.instance变量为空(理由见3.3.1),wrapper.allocate的会oadServlet加载。