此处利用了https://github.com/jeonghanlee/epicsarchiverap-env将AA安装在/opt/epicsarchiverap/下,并以系统服务的方式启停。
问题产生原因
地球人都知道,URL请求:xxx:17665/mgmt/ui/index.html用于访问AA管理页面。这样很不安全,希望更改Context path,将mgmt改为别的字符串,来隐藏AA管理页面。
修改/opt/epicsarchiverap/mgmt/conf/server.xml文件中的Context属性:
...
<Host ...>
<Context docBase="mgmt" path="/hahaha"></Context> # 加入此行,将URL请求中原本是mgmt的部分改为hahaha
...
</Host>
重启AA服务,在浏览器导航栏输入URL请求:xxx:17665/hahaha/ui/index.html,报错500。查看日志文件,/opt/epicsarchiverap/mgmt/logs/localhost.2022-07-19.log中报空指针异常:
19-Jul-2022 09:52:24.307 SEVERE [http-nio-17665-exec-5] org.apache.catalina.core.StandardWrapperValve.invoke Servlet.service() for servlet [StaticContent] in context with path [/hahaha] threw exception
java.lang.NullPointerException
at org.epics.archiverappliance.mgmt.MgmtUIFilter.doFilter(MgmtUIFilter.java:53)
at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:189)
at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:162)
at org.apache.catalina.core.StandardWrapperValve.invoke(StandardWrapperValve.java:197)
at org.apache.catalina.core.StandardContextValve.invoke(StandardContextValve.java:97)
at org.apache.catalina.authenticator.AuthenticatorBase.invoke(AuthenticatorBase.java:542)
at org.apache.catalina.core.StandardHostValve.invoke(StandardHostValve.java:135)
at org.apache.catalina.valves.ErrorReportValve.invoke(ErrorReportValve.java:92)
at org.apache.catalina.valves.AbstractAccessLogValve.invoke(AbstractAccessLogValve.java:687)
at org.apache.catalina.core.StandardEngineValve.invoke(StandardEngineValve.java:78)
at org.apache.catalina.connector.CoyoteAdapter.service(CoyoteAdapter.java:357)
at org.apache.coyote.http11.Http11Processor.service(Http11Processor.java:382)
at org.apache.coyote.AbstractProcessorLight.process(AbstractProcessorLight.java:65)
at org.apache.coyote.AbstractProtocol$ConnectionHandler.process(AbstractProtocol.java:893)
at org.apache.tomcat.util.net.NioEndpoint$SocketProcessor.doRun(NioEndpoint.java:1726)
at org.apache.tomcat.util.net.SocketProcessorBase.run(SocketProcessorBase.java:49)
at org.apache.tomcat.util.threads.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1191)
at org.apache.tomcat.util.threads.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:659)
at org.apache.tomcat.util.threads.TaskThread$WrappingRunnable.run(TaskThread.java:61)
at java.base/java.lang.Thread.run(Thread.java:834)
发现是在调用org.epics.archiverappliance.mgmt.MgmtUIFilter的doFilter()方法时,报的空指针异常。MgmtUIFilter.java的第53行:
52 MgmtRuntimeState runtime = configService.getMgmtRuntimeState();
53 if(runtime.haveChildComponentsStartedUp()) {
54 chain.doFilter(request, response);
55 ...
那么就是此处的runtime是空指针。runtime来自哪里呢?在源码包epicsarchiverap-master\src\main\org\epics\archiverappliance\config\DefaultConfigService.java中找到答案:
...
protected MgmtRuntimeState mgmtRuntime = null;
...
@Override
public void initialize(ServletContext sce) throws ConfigException {
this.servletContext = sce;
String contextPath = sce.getContextPath();
logger.info("DefaultConfigService was created with a servlet context " + contextPath);
...
switch(contextPath) {
case "/mgmt":
warFile = WAR_FILE.MGMT;
this.mgmtRuntime = new MgmtRuntimeState(this);
break;
...
default:
logger.error("We seem to have introduced a new component into the system " + contextPath);
}
...
}
@Override
public MgmtRuntimeState getMgmtRuntimeState() {
return mgmtRuntime;
}
可见,由于此时的Context属性中,contextPath被设置为了“hahaha",所以在initialize()方法中不会再执行this.mgmtRuntime = new MgmtRuntimeState(this);
因此mgmtRuntime一直是空指针。
查看/opt/epicsarchiverap/mgmt/logs/archappl_service.log日志文件也可证明contextPath被设置为了“hahaha":
83 [main] INFO org.epics.archiverappliance.config.DefaultConfigService - DefaultConfigService was created with a servlet context /hahaha
...
97 [main] ERROR org.epics.archiverappliance.config.DefaultConfigService - We seem to have introduced a new component into the system /hahaha
那么,最根本的问题:为什么会调用到org.epics.archiverappliance.mgmt.MgmtUIFilter.doFilter()方法?
这是因为在/opt/epicsarchiverap/mgmt/webapps/mgmt/WEB-INF/web.xml文件中,定义了一个过滤器:
<web-app>
<filter>
<filter-name>MgmtUIFilter</filter-name>
<filter-class>org.epics.archiverappliance.mgmt.MgmtUIFilter</filter-class>
</filter>
...
<filter-mapping>
<filter-name>MgmtUIFilter</filter-name>
<url-pattern>/ui/*</url-pattern>
<dispatcher>REQUEST</dispatcher>
</filter-mapping>
...
在此名为“hahaha”的web应用(Context)中,所有类似于“/ui/*”的URL请求都需要经过此过滤器过滤。因此,我们输入URL请求:xxx:17665/hahaha/ui/index.html时,首先会经过MgmtUIFilter过滤器的doFilter()方法进行过滤。
解决方法
由于在web应用初始化过程中,需要对ConfigService(储存了该web应用的相关信息)进行初始化,然而其初始化代码中,只对标准的Context path(mgmt、etl、engine、retrieve)进行了初始化操作,对于我们自定义的Context path(如“hahaha”)缺少了很多初始化信息,这些信息指针为空。而后续的Servlet(如MgmtUIFilter、StaticContentServlet等)中会使用到这些缺少的初始化信息,因此解决办法还是应该使这些信息得到初始化。
目前的DefaultConfigService.java源码不支持我们修改mgmt、etl、engine、retrieve这4个web应用的Context属性,可以修改DefaultConfigService.java这部分的代码,使我们自定义的Context path也可以得到相应的初始化。
还有一种治标不治本的方法,就是去掉mgmt/WEB-INF/web.xml文件中的过滤器部分,也就是不添加MgmtUIFilter过滤器。重启AA后,URL请求:xxx:17665/hahaha/ui/index.html可以正常打开该web页面。但这种解决方法坏处在于:
- 未解决其他Servlet(如StaticContentServlet等)的信息空指针异常问题,后续HTTP请求及web页面还是会出问题。如:
19-Jul-2022 11:15:47.172 SEVERE [http-nio-17665-exec-10] org.apache.catalina.core.StandardWrapperValve.invoke Servlet.service() for servlet [StaticContent] in context with path [/hahaha] threw exception java.lang.NullPointerException at org.epics.archiverappliance.common.StaticContentServlet$PathSequence.templateReplace(StaticContentServlet.java:475)
- MgmtUIFilter主要作用是对ConfigService是否正确初始化以及其他几个web应用(etl、engine、retrieve)是否已启动完成的查验,若这些查验不通过,则返回错误信息给web页面,避免进一步的HTTP请求和处理。MgmtUIFilter实际上是对AA服务的一种保护,将其删掉不合适。
附加内容
DefaultConfigService 的创建及初始化时间,是在整个web应用初始化时。
来自epicsarchiverap-master\src\main\org\epics\archiverappliance\config\ArchServletContextListener.java:
public class ArchServletContextListener implements ServletContextListener {
...
@Override
public void contextInitialized(ServletContextEvent sce) {
...
configService = new DefaultConfigService();
...
configService.initialize(sce.getServletContext());
sce.getServletContext().setAttribute(ConfigService.CONFIG_SERVICE_NAME, configService);
...
来自/opt/epicsarchiverap/mgmt/webapps/mgmt/WEB-INF/web.xml:
<listener>
<listener-class>org.epics.archiverappliance.config.ArchServletContextListener</listener-class>
</listener>
Tomcat 的 Filter定义、初始化、调用
- Filter编写和注册:Tomcat过滤器
- Filter和FilterChain初始化:【Tomcat】Filter 原理
- Filter和FilterChain的调用:Http请求到达后进行的处理——StandardWrapperValve的invoke方法创建一个ApplicationFilterChain;接着调用FilterChain的doFilter方法,对filter进行逐个调用,触发其生命周期事件;doFilter方法最后调用了该Http请求对应的Servlet的service方法,所有的内容都以Response返回。浅析Tomcat之StandardWrapperValve的Servlet请求处理