前言
上個星期碰到個客戶使用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檔案然後作相應處理後快取到檔案中,存在和上面程式碼同樣的問題,暫未找到更好的解決方案