问题描述

    上个星期碰到个客户使用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文件然后作相应处理后缓存到文件中,存在和上面代码同样的问题,暂未找到更好的解决方案