whip1ash

Drupal 8.5.0 源码分析 (启动流程/路由分发)

2019-04-08

0x01 Abstract

比较详细的Drupal启动以及HTTP处理、路由分发分析。避免调漏洞的时候一脸懵逼。
纸上得来终觉浅,绝知此事要躬行。

0x02 Background

环境准备

源码从这里下载
PHP版本是7.1.2

背景知识

Drupal是建立在Symfony框架之上的,详细结构在这里。Drupal的路由系统是建立在Symfony内核之上的,下图是路由和控制器的简单示意图:

还有一个详细的请求处理和渲染的流程图,图片太大,不挂了,大图在这里

0x03 一切还从index.php开始

打开index.php,只有简洁的几行代码,一行一行看。
-w752

require_once ‘autoload.php’

跟进,来到vendor/autoload.php,这里包含了vendor/composer/autoload_real.php并通过ComposerAutoloaderInitDrupal8的getLoader()获取了一个loader。
-w686
跟进getLoader();

-w1363

接上图
-w1050

这样就获得了一个自动类加载器,通过这个自动类加载器中定义的基本关系去查找函数和类定义文件,不需要大量include了。

注1

关于类的自动加载在这里
这里用array去加载ComposerAutoloaderInitDrupal8这个类的loadClassLoader方法。是Callback/Callable类型的一个特性。
-w1074

Demo:

autoload.php

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<?php

Class autoload_test{
public static function loadClassLoader($class){
if ('Test' === $class){
require __DIR__.'/Test.php';
}
}
public static function getLoader() {
spl_autoload_register(array(autoload_test,loadClassLoader),true,true);
$Test = new Test();
}
}

autoload_test::getLoader()
?>

Test.php

1
2
3
4
5
6
7
<?php
class Test{
function __construct(){
echo "This is class test";
}
}
?>

注2

PSR(PHP Standards Recommendation):PHP 推荐规范的简称。是一种代码格式规范。
关于PSR0和PSR4 在这里

new DrupalKernel(‘prod’, $autoloader);

获得DrupalKernel对象,DrupalKernel这个类是Drupal的核心。这个类构建了一个依赖注入容器,并且处理服务提供者的注册(registration of service providers)。对于这个依赖注入容器,我的理解是,容器中的某个属性指向某个依赖实例化的对象,这么讲很拗口。如图。

看起来非常简单,实现起来也非常简单,就是这个名字起得让人看起来感觉得不简单。
下图是该类的构造函数。
-w1292

Request::createFromGlobals();

通过Request类的静态方法获得封装好的request对象。

-w1358
$request->header->get()方法后面很常用,可以跟进去看一下。工厂方法的封装也可以看一下,封装的时候,GET请求对应$query变量,POST请求对应$request变量。

\$kernel->handle(\$request);

上述内容基本就是准备工作,从这里开始正式处理HTTP请求。
-w1328

注1

PHP_SAPI PHP常见的四种运行模式

$this->boot()

boot方法主要是初始化了FileCacheFactory,但是我并不关心这一点,我关心container是如何初始化的。

-w1354

初始化的过程比较复杂,这里只看最初的情况,也就是通过容器定义创建一个新容器。在initializeContainer()方法中,容器定义是通过如下图的方式获取的。
-w1047

服务初始化加载

容器的定义中存在服务加载的流程,跟进看看怎么实现的,后面分发消息时频繁调用服务。

这里get方法获取bootstrapContainer依赖的一个属性,动态跟一下。
$this->bootstrapContainer指向Drupal\Component\DependencyInjection\Container对象。

core/lib/Drupal/Component/DependencyInjection/Container.php
-w1447
-w494
接上代码图,在这里创建服务。

跟进。

获取传入定义的参数,文件,工厂方法等。

通过不同的参数个数进行实例化然后返回并跳回到getCachedContainerDefinition函数中。
现在已经通过get(‘cache.container’)获得了一个Drupal\Core\Cache\Backend的对象,然后再调用它的get方法,参数为getContainerCacheKey()方法返回。

跟进。

Service的数据是从数据库中查询出来的,其实Drupal的很多配置信息,比如序列化数据等都是通过数据库查询出来的,如果Drupal存在SQLi的话,将直接可能导致RCE。
跟进prepareItem函数。

将数据反序列化。
-w536
所有的服务信息都在这里储存,使用序列化数组的形式,在使用时候反序列化得到配置数组,然后调用创建服务模块,直接动态实例化相应的类。

创建新容器

拥有了容器定义后,创建新容器就很简单了。
-w1350

也就是在最初的情况下,当前$this->container是一个\Drupal\Core\DependencyInjection\Container(当设置中的container_base_class选项为空时)。

\$response = \$this->getHttpKernel()->handle(\$request, \$type, $catch);

看一下getHttpKernel()方法.
-w783
这里的$this->container就是\Drupal\Core\DependencyInjection\Container。这里的Container的定义如下图。
这里就是上面分析的创建服务的流程,不再赘述。
一直跟进handle方法,跟进Drupal\page_cache\StackMiddleware\PageCache的handle方法。

$respones等于$this->lookup()返回的结果,跟进。

此处判断是否存在缓存,还是通过Drupal\Core\Cache\DatabaseBackend来返回结果,也就是说页面的缓存数据也存在于数据库中,如果击中缓存,添加一个击中缓存的HTTP头,返回response。

将数据库中cache_page表相应字段的数据清空,走进fetch逻辑。

fetch逻辑

进入fetch函数后,进行了很多预处理,比如设置相应的属性启动session等,与前面基本相同,故不再赘述,直接跟到HttpKernel->handleRaw()方法。

handleRaw()方法代码较长,分块来看。
-w940
这一块触发Request事件,判断是否有响应,如果有response返回的话直接返回。进入消息分发逻辑。
-w1480

当前事件为kernel.request,查看当前$this变量。

listeners属性中,存在19个不同事件的监听器(订阅者),每个订阅者中不同的key代表不同的优先级(猜测),数字越大,优先级越大,越先被处理。
每当产生一个事件,都会遍历调用(使用call_user_func)监听这个事件的订阅者的相关逻辑,通过判断这个事件的一些属性,来选择是否对此事件带来的$request(相关对象)进行处理,在处理完后可以选择结束这个事件,将事件的属性设置为结束就会跳入结束流程。

订阅者处理的逻辑比较多,不一一展开,跟一下dynamic_page_cache_subscriber服务。
-w1016
-w1628
-w1563

此时的$this指向的是PlaceholderingRenderCache,也就是当前函数所在类的子类。但是在PlaceholderingRenderCache->get()中又会调用parent::get方法。第一次获取缓存拿到响应的配置信息(序列化数据),第二次调用获取到相应的数据。

这些缓存数据是从cache_dynamic_page表中获取到的,清空这个表。
回到事件分发逻辑,跟进kernel.request事件下的router.route_preloader服务的onRequest方法。
对于路由进行预处理。
-w1466
这个事件的处理就结束了,回到HttpKernel。
-w1634
触发控制器事件和控制器参数事件。

0x04 Exception

在此处花费了大概三四天的时间,已经远远超出我的预期,所以就此打住了,可能以后还会更,可能不会更了。

其实源码分析到这里就基本可以结束了,后续所进行的无非就是不同的事件,调用不同的服务,在其中有不同的逻辑罢了,后面有对于请求会使用Exception进行修正,然后渲染返回。理解了Drupal的容器,服务和事件的逻辑,后面不过是来回调用而已。

Reference

  1. https://github.com/dreadlocked/Drupalgeddon2
  2. http://blog.topsec.com.cn/archives/3743

扫描二维码,分享此文章