Swoole Compiler 加密 Drupal 產生的一些問題

前言

上個星期碰到個客戶使用Swoole Compiler加密Drupal致使Drupal項目沒法運行的問題,逐步排查後總結問題是Drupal中有部分代碼直接經過file_get_contents獲取PHP源碼致使的,由於項目代碼是加密事後的,因此直接獲取PHP源碼解析是獲取不到想要的內容的。
注:php

  • Swoole Compilerhttps://www.swoole-cloud.com/compiler.html
  • Drupal是使用PHP語言編寫的開源內容管理框架(CMF),它由內容管理系統(CMS)和PHP開發框架(Framework)共同構成。

加密後的影響Drupal運行的主要代碼

代碼路徑

drupal/vendor/doctrine/common/lib/Doctrine/Common/Reflection/StaticReflectionParser.php:126html

//代碼內容
    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類中構造函數以下sql

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就解析不到正確的信息,從而致使程序沒法運行。express

解決方案

本次使用的 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文件。swoole

代碼路徑:drupal/vendor/symfony/class-loader/ClassCollectionLoader.php:126
此類中是Symfony讀取PHP文件而後做相應處理後緩存到文件中,存在和上面代碼一樣的問題,暫未找到更好的解決方案框架

相關文章
相關標籤/搜索