共计 7362 个字符,预计需要花费 19 分钟才能阅读完成。
前言
上个星期碰到个客户使用 Swoole Compiler
加密 Drupal
导致 Drupal
项目无法运行的问题,逐步排查后总结问题是 Drupal
中有部分代码直接通过 file_get_contents
获取 PHP
源码导致的,因为项目代码是加密过后的,所以直接获取 PHP
源码解析是获取不到想要的内容的。
注:
-
Swoole Compiler
:https://www.swoole-cloud.com/compiler.html -
Drupal
是使用 PHP 语言编写的开源内容管理框架(CMF
),它由内容管理系统(CMS
)和PHP
开发框架(Framework
)共同构成。
加密后的影响 Drupal
运行的主要代码
代码路径
drupal/vendor/doctrine/common/lib/Doctrine/Common/Reflection/StaticReflectionParser.php:126
// 代码内容
protected function parse()
{if ($this->parsed || !$fileName = $this->finder->findFile($this->className)) {return;}
$this->parsed = true;
$contents = file_get_contents($fileName);
if ($this->classAnnotationOptimize) {if (preg_match("/\A.*^\s*((abstract|final)\s+)?class\s+{$this->shortClassName}\s+/sm", $contents, $matches)) {$contents = $matches[0];
}
}
$tokenParser = new TokenParser($contents);
......
}
其中部分代码如上,通过 class
名获取文件路径,然后通过 file_get_contents
获取 PHP
文件的内容,其中 TokenParser
类中构造函数如下
public function __construct($contents)
{$this->tokens = token_get_all($contents);
token_get_all("<?php\n/**\n *\n */");
$this->numTokens = count($this->tokens);
}
传入获取到的源码通过 token_get_all
进行解析,然后后续分析代码获取 PHP
文件的类、属性、方法的注释,父类的命名空间 和 class
名,本类的 use
信息等,因为文件已经加密,所以 file_get_contents
获取到的内容是加密后的内容,token_get_all
就解析不到正确的信息,从而导致程序无法运行。
解决方案
本次使用的
2.1.1
版本的加密器,通过Swoole Compiler
加密器加密的代码,在配置文件中save_doc
配置选项必须设置为1
,如果设置为0
则不会保存注释,并且在2.1.3
版本swoole_loader.so
扩展中新增加的函数naloinwenraswwww
也无法获取到类中 use 的相关信息,具体函数使用在后面会详细说明。
1 $ref = new \ReflectionClass($this->className);
2
3 $parent_ref = $ref->getParentClass();
4
5 ......
6
7 if (is_file($fileName)) {8 $php_file_info = unserialize(naloinwenraswwww(realpath($fileName)));
9 foreach ($php_file_info as $key => $info) {10 if ($key == 'swoole_namespaces' || $key == 'swoole_class_name') {
11 continue;
12 }
13 $this->useStatements[$key] = $info;
14 }
15 }
16
17 $this->parentClassName = $parent_ref->getName();
18
19 if (strpos($this->parentClassName, '\\')!==0) {
20 $this->parentClassName = '\\'.$this->parentClassName;
21 }
22
23 $static_properties = [];
24
25 $properties = $ref->getProperties();
26
27 $parent_properties = $this->createNewArrKey($parent_ref->getProperties());
28
29 ......
30
31 $static_methods = [];
32
33 $methods = $ref->getMethods();
34
35 ......
- 第 1 行通过类名来获取反射类
ReflectionClass
类的对象。 - 因为此反射类包含了所有父类中的属性和方法,但源码中只要获取本类中的属性和方法,所以还要获取父类的反射类然后通过对比来剔除父类中的属性和方法,第 3 行使用
ReflectionClass
类提供的getParentClass
方法获取父类的反射类,此方法返回父类的ReflectionClass
对象。 - 第 25 行通过
ReflectionClass
类提供的getProperties
方法分别获取本类和父类中的属性,然后进行对比剔除父类的属性,保留本类的属性,此方法返回的是一个ReflectionProperty
类对象。 - 通过
ReflectionProperty
类提供的getDocComment
方法就可以拿到属性的注释。 - 同上第 33 行通过
ReflectionClass
类提供的getMethods
方法可以拿到本类和父类中的方法,然后进行对比剔除父类的方法,保留本类的方法,此方法返回的是一个ReflectionMethod
类对象。 - 通过
ReflectionMethod
对象提供的getDocComment
方法就可以拿到方法的注释。 - 通过第 17 行
ReflectionClass
提供的getName
方法可以拿到类名。
因为反射无法获取
use
类的信息,所以在2.1.3
版本中的swoole_loader.so
扩展中添加函数naloinwenraswwww
,此函数传入一个PHP
文件的绝对路径,返回传入文件的相关信息的序列化数组,反序列化后数组如下
[
"swoole_namespaces" => "Drupal\Core\Datetime\Element",
"swoole_class_name" => "Drupal\Core\Datetime\Element\DateElementBase",
"nestedarray" => "Drupal\Component\Utility\NestedArray",
"drupaldatetime" => "Drupal\Core\Datetime\DrupalDateTime",
"formelement"=> "Drupal\Core\Render\Element\FormElement"
]
其中 swoole_namespaces
为文件的命名空间,swoole_class_name
为文件的命名空间加类名,其他为 use
信息,键为 use
类的类名小写字母,如存在别名则为别名的小写字母,值为 use
类的命名空间加类名,通过该函数和反射函数可以兼容 StaticReflectionParser
中加密后出现的无法获取正确信息的问题
在加密后的未影响 Drupal
运行的潜在问题:
- 代码路径:
drupal/vendor/doctrine/annotations/lib/Doctrine/Common/Annotations/PhpParser.php:39
- 代码路径:
drupal/vendor/symfony/class-loader/ClassMapGenerator.php:91
- 代码路径:
drupal/vendor/symfony/routing/Loader/AnnotationFileLoader.php:90
Drupal
中引入了Symfony
框架,此框架中部分代码也是通过file_get_contents
和token_get_all
来获取PHP
文件的类名,但目前未对Druapl
运行产生影响,可能并未用到其中方法
解决方案:
同 StaticReflectionParser
类的解决方案一样通过 2.1.3
版本中的 swoole_loader.so
扩展中添加函数 naloinwenraswwww
来获取加密后文件的命名空间和类名
尚未有更好方案的问题:
- 代码路径:
drupal/core/includes/install.inc:220
function drupal_rewrite_settings($settings = [], $settings_file = NULL)
{if (!isset($settings_file)) {$settings_file = \Drupal::service('site.path') . '/settings.php';
}
// Build list of setting names and insert the values into the global namespace.
$variable_names = [];
$settings_settings = [];
foreach ($settings as $setting => $data) {if ($setting != 'settings') {_drupal_rewrite_settings_global($GLOBALS[$setting], $data);
} else {_drupal_rewrite_settings_global($settings_settings, $data);
}
$variable_names['$' . $setting] = $setting;
}
$contents = file_get_contents($settings_file);
if ($contents !== FALSE) {
// Initialize the contents for the settings.php file if it is empty.
if (trim($contents) === '') {$contents = "<?php\n";}
// Step through each token in settings.php and replace any variables that
// are in the passed-in array.
$buffer = '';
$state = 'default';
foreach (token_get_all($contents) as $token) {if (is_array($token)) {list($type, $value) = $token;
} else {
$type = -1;
$value = $token;
}
// Do not operate on whitespace.
if (!in_array($type, [T_WHITESPACE, T_COMMENT, T_DOC_COMMENT])) {switch ($state) {
case 'default':
if ($type === T_VARIABLE && isset($variable_names[$value])) {
// This will be necessary to unset the dumped variable.
$parent = &$settings;
// This is the current index in parent.
$index = $variable_names[$value];
// This will be necessary for descending into the array.
$current = &$parent[$index];
$state = 'candidate_left';
}
break;
case 'candidate_left':
if ($value == '[') {$state = 'array_index';}
if ($value == '=') {$state = 'candidate_right';}
break;
case 'array_index':
if (_drupal_rewrite_settings_is_array_index($type, $value)) {$index = trim($value, '\'"');
$state = 'right_bracket';
} else {// $a[foo()] or $a[$bar] or something like that.
throw new Exception('invalid array index');
}
break;
case 'right_bracket':
if ($value == ']') {if (isset($current[$index])) {
// If the new settings has this index, descend into it.
$parent = &$current;
$current = &$parent[$index];
$state = 'candidate_left';
} else {
// Otherwise, jump back to the default state.
$state = 'wait_for_semicolon';
}
} else {// $a[1 + 2].
throw new Exception('] expected');
}
break;
case 'candidate_right':
if (_drupal_rewrite_settings_is_simple($type, $value)) {$value = _drupal_rewrite_settings_dump_one($current);
// Unsetting $current would not affect $settings at all.
unset($parent[$index]);
// Skip the semicolon because _drupal_rewrite_settings_dump_one() added one.
$state = 'semicolon_skip';
} else {$state = 'wait_for_semicolon';}
break;
case 'wait_for_semicolon':
if ($value == ';') {$state = 'default';}
break;
case 'semicolon_skip':
if ($value == ';') {
$value = '';
$state = 'default';
} else {
// If the expression was $a = 1 + 2; then we replaced 1 and
// the + is unexpected.
throw new Exception('Unexpected token after replacing value.');
}
break;
}
}
$buffer .= $value;
}
foreach ($settings as $name => $setting) {$buffer .= _drupal_rewrite_settings_dump($setting, '$' . $name);
}
// Write the new settings file.
if (file_put_contents($settings_file, $buffer) === FALSE) {throw new Exception(t('Failed to modify %settings. Verify the file permissions.', ['%settings' => $settings_file]));
} else {
// In case any $settings variables were written, import them into the
// Settings singleton.
if (!empty($settings_settings)) {$old_settings = Settings::getAll();
new Settings($settings_settings + $old_settings);
}
// The existing settings.php file might have been included already. In
// case an opcode cache is enabled, the rewritten contents of the file
// will not be reflected in this process. Ensure to invalidate the file
// in case an opcode cache is enabled.
OpCodeCache::invalidate(DRUPAL_ROOT . '/' . $settings_file);
}
} else {throw new Exception(t('Failed to open %settings. Verify the file permissions.', ['%settings' => $settings_file]));
}
}
Drupal
安装过程中有个配置文件 default.setting.php
,里面存放了默认配置数组,在安装的过程中会让用户在安装界面输入一些配置比如Mysql
的信息,输入过后此方法通过 file_get_contents
和token_get_all
来获取 setting
中的信息,然后合并用户在页面输入的信息,重新存回文件,因为整个过程涉及到读取文件,更改文件信息,在存入文件,所以 Swoole Compiler
在此处暂时没有更好的解决方案,需要在加密的时候选择不加密 setting
文件。
代码路径:drupal/vendor/symfony/class-loader/ClassCollectionLoader.php:126
此类中是 Symfony
读取 PHP 文件然后作相应处理后缓存到文件中,存在和上面代码同样的问题,暂未找到更好的解决方案