乐趣区

Swoole-Compiler-加密-Drupal-产生的一些问题

前言

上个星期碰到个客户使用 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. 第 1 行通过类名来获取反射类 ReflectionClass 类的对象。
  2. 因为此反射类包含了所有父类中的属性和方法,但源码中只要获取本类中的属性和方法,所以还要获取父类的反射类然后通过对比来剔除父类中的属性和方法,第 3 行使用 ReflectionClass 类提供的 getParentClass 方法获取父类的反射类,此方法返回父类的 ReflectionClass 对象。
  3. 第 25 行通过 ReflectionClass 类提供的 getProperties 方法分别获取本类和父类中的属性,然后进行对比剔除父类的属性,保留本类的属性,此方法返回的是一个 ReflectionProperty 类对象。
  4. 通过 ReflectionProperty 类提供的 getDocComment 方法就可以拿到属性的注释。
  5. 同上第 33 行通过 ReflectionClass 类提供的 getMethods 方法可以拿到本类和父类中的方法,然后进行对比剔除父类的方法,保留本类的方法,此方法返回的是一个 ReflectionMethod 类对象。
  6. 通过 ReflectionMethod 对象提供的 getDocComment 方法就可以拿到方法的注释。
  7. 通过第 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_contentstoken_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_contentstoken_get_all来获取 setting 中的信息,然后合并用户在页面输入的信息,重新存回文件,因为整个过程涉及到读取文件,更改文件信息,在存入文件,所以 Swoole Compiler 在此处暂时没有更好的解决方案,需要在加密的时候选择不加密 setting 文件。

代码路径:drupal/vendor/symfony/class-loader/ClassCollectionLoader.php:126
此类中是 Symfony 读取 PHP 文件然后作相应处理后缓存到文件中,存在和上面代码同样的问题,暂未找到更好的解决方案

退出移动版