问题描述
上个星期碰到个客户使用swoole compiler加密drupal导致drupal项目无法运行的问题,逐步排查后总结问题是drupal中有部分代码直接通过file_get_contents获取php源码导致的,因为项目代码是加密过后的,所以直接获取php源码解析是获取不到想要的内容的。
加密框架drupal
Drupal是使用PHP语言编写的开源内容管理框架,下载完后需要进行相关的配置和安装才能进行使用。
在加密后的影响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); $docComment = ''; while ($token = $tokenParser->next(false)) { if (is_array($token)) { switch ($token[0]) { case T_USE: $this->useStatements = array_merge($this->useStatements, $tokenParser->parseUseStatement()); break; case T_DOC_COMMENT: $docComment = $token[1]; break; case T_CLASS: $this->docComment['class'] = $docComment; $docComment = ''; break; case T_VAR: case T_PRIVATE: case T_PROTECTED: case T_PUBLIC: $token = $tokenParser->next(); if ($token[0] === T_VARIABLE) { $propertyName = substr($token[1], 1); $this->docComment['property'][$propertyName] = $docComment; continue 2; } if ($token[0] !== T_FUNCTION) { // For example, it can be T_FINAL. continue 2; } // No break. case T_FUNCTION: // The next string after function is the name, but // there can be & before the function name so find the // string. while (($token = $tokenParser->next()) && $token[0] !== T_STRING); $methodName = $token[1]; $this->docComment['method'][$methodName] = $docComment; $docComment = ''; break; case T_EXTENDS: $this->parentClassName = $tokenParser->parseClass(); $nsPos = strpos($this->parentClassName, '\\'); $fullySpecified = false; if ($nsPos === 0) { $fullySpecified = true; } else { if ($nsPos) { $prefix = strtolower(substr($this->parentClassName, 0, $nsPos)); $postfix = substr($this->parentClassName, $nsPos); } else { $prefix = strtolower($this->parentClassName); $postfix = ''; } foreach ($this->useStatements as $alias => $use) { if ($alias == $prefix) { $this->parentClassName = '\\' . $use . $postfix; $fullySpecified = true; } } } if (!$fullySpecified) { $this->parentClassName = '\\' . $this->namespace . '\\' . $this->parentClassName; } break; } } } }
其中部分代码如上,通过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就解析不到正确的信息。
解决方案:
通过反射函数直接获取类的相关注释,和父类的信息,因为反射无法获取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中引入了Symfony框架,此框架中部分代码也是通过file_get_contents和token_get_all来获取php文件的类名,但目前未对druapl运行产生影响,可能并未用到其中方法
代码路径:drupal/vendor/symfony/class-loader/ClassMapGenerator.php:91
代码路径:drupal/vendor/symfony/routing/Loader/AnnotationFileLoader.php:90
解决方案:同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在此处暂时没有更好的解决方案。
代码路径:drupal/vendor/symfony/class-loader/ClassCollectionLoader.php:126
此类中是symfony读取php文件然后作相应处理后缓存到文件中,存在和上面代码同样的问题,暂未找到更好的解决方案