1
0
mirror of https://github.com/danog/MadelineProto.git synced 2024-11-30 07:58:58 +01:00

Include plugins before deserializing session

This commit is contained in:
Daniil Gentili 2023-07-13 16:02:53 +02:00
parent 3e8047cd1b
commit b9a9aa8af3
Signed by: danog
GPG Key ID: 8C1BE3B34B230CA7
5 changed files with 131 additions and 105 deletions

View File

@ -186,6 +186,11 @@ class MyEventHandler extends SimpleEventHandler
{
$message->reply('pong');
}
public static function getPluginPaths(): string|array|null
{
return 'plugins/';
}
}
$settings = new Settings;

View File

@ -35,19 +35,7 @@ use danog\MadelineProto\EventHandler\Filter\Filter;
use danog\MadelineProto\EventHandler\Filter\FilterAllowAll;
use danog\MadelineProto\EventHandler\Update;
use Generator;
use mysqli;
use PDO;
use PhpParser\Node\Expr\FuncCall;
use PhpParser\Node\Expr\Include_;
use PhpParser\Node\Expr\New_;
use PhpParser\Node\Name;
use PhpParser\Node\Scalar\LNumber;
use PhpParser\Node\Scalar\String_;
use PhpParser\Node\Stmt\DeclareDeclare;
use PhpParser\NodeFinder;
use PhpParser\NodeVisitor\NameResolver;
use PhpParser\ParserFactory;
use PHPStan\PhpDocParser\Ast\NodeTraverser;
use ReflectionAttribute;
use ReflectionClass;
use ReflectionMethod;
@ -57,7 +45,6 @@ use Webmozart\Assert\Assert;
use function Amp\File\isDirectory;
use function Amp\File\isFile;
use function Amp\File\listFiles;
use function Amp\File\read;
/**
* Event handler.
@ -81,6 +68,7 @@ abstract class EventHandler extends AbstractAPI
if (self::$includingPlugins) {
return;
}
static::internalGetDirectoryPlugins();
$settings ??= new SettingsEmpty;
$API = new API($session, $settings);
$API->startAndLoopInternal(static::class);
@ -99,6 +87,7 @@ abstract class EventHandler extends AbstractAPI
if (self::$includingPlugins) {
return;
}
static::internalGetDirectoryPlugins();
$settings ??= new SettingsEmpty;
$API = new API($session, $settings);
$API->botLogin($token);
@ -221,7 +210,7 @@ abstract class EventHandler extends AbstractAPI
};
}
if ($this instanceof SimpleEventHandler) {
self::validateEventHandler(static::class);
Tools::validateEventHandlerClass(static::class);
}
if ($has_any) {
$onAny = $this->onAny(...);
@ -290,7 +279,7 @@ abstract class EventHandler extends AbstractAPI
*
* @return non-empty-string|non-empty-list<non-empty-string>|null
*/
public function getPluginPaths(): string|array|null
public static function getPluginPaths(): string|array|null
{
return null;
}
@ -299,7 +288,7 @@ abstract class EventHandler extends AbstractAPI
*
* @return array<class-string<EventHandler>>
*/
public function getPlugins(): array
public static function getPlugins(): array
{
return [];
}
@ -309,31 +298,31 @@ abstract class EventHandler extends AbstractAPI
*
* @return list<class-string<PluginEventHandler>>
*/
private function internalGetPlugins(): array
private static function internalGetPlugins(): array
{
$plugins = $this->getPlugins();
$plugins = static::getPlugins();
$plugins = \array_values(\array_unique($plugins, SORT_REGULAR));
$plugins = \array_merge($plugins, $this->internalGetDirectoryPlugins($plugins));
$plugins = \array_merge($plugins, static::internalGetDirectoryPlugins());
foreach ($plugins as $plugin) {
Assert::classExists($plugin);
Assert::true(\is_subclass_of($plugin, PluginEventHandler::class), "$plugin must extend ".PluginEventHandler::class);
Assert::notEq($plugin, PluginEventHandler::class);
Assert::true(\str_contains(\ltrim($plugin, '\\'), '\\'), "$plugin must be in a namespace!");
self::validateEventHandler($plugin);
Tools::validateEventHandlerClass($plugin);
}
return $plugins;
}
private static array $checkedPaths = [];
private function internalGetDirectoryPlugins(): array
private static function internalGetDirectoryPlugins(): array
{
if ($this instanceof PluginEventHandler) {
if (is_subclass_of(static::class, PluginEventHandler::class)) {
return [];
}
$paths = $this->getPluginPaths();
$paths = static::getPluginPaths();
if (\is_string($paths)) {
$paths = [$paths];
} elseif ($paths === null) {
@ -415,84 +404,4 @@ abstract class EventHandler extends AbstractAPI
return $plugins;
}
private const BANNED_FUNCTIONS = [
'file_get_contents',
'file_put_contents',
'unlink',
'curl_exec',
'mysqli_query',
'mysqli_connect',
'mysql_connect',
'fopen',
'fsockopen',
];
private const BANNED_FILE_FUNCTIONS = [
'amp\\file\\read',
'amp\\file\\write',
'amp\\file\\get',
'amp\\file\\put',
];
private const BANNED_CLASSES = [
PDO::class,
mysqli::class,
];
/**
* Perform static analysis on a certain event handler class, to make sure it satisfies some performance requirements.
*
* @param class-string<EventHandler> $class Class name
*
* @throws AssertionError If validation fails.
*/
final public static function validateEventHandler(string $class): void
{
$file = read((new ReflectionClass($class))->getFileName());
$file = (new ParserFactory)->create(ParserFactory::ONLY_PHP7)->parse($file);
Assert::notNull($file);
$traverser = new NodeTraverser([new NameResolver()]);
$file = $traverser->traverse($file);
$finder = new NodeFinder;
/** @var DeclareDeclare|null $call */
$declare = $finder->findFirstInstanceOf($file, DeclareDeclare::class);
if ($declare === null
|| $declare->key->name !== 'strict_types'
|| !$declare->value instanceof LNumber
|| $declare->value->value !== 1
) {
throw new AssertionError("An error occurred while analyzing plugin $class: for performance reasons, the first statement of a plugin must be declare(strict_types=1);");
}
/** @var FuncCall $call */
foreach ($finder->findInstanceOf($file, FuncCall::class) as $call) {
if (!$call->name instanceof Name) {
continue;
}
$name = $call->name->toLowerString();
if (\in_array($name, self::BANNED_FUNCTIONS, true)) {
throw new AssertionError("An error occurred while analyzing plugin $class: for performance reasons, plugins may not use the non-async blocking function $name!");
}
if (\in_array($name, self::BANNED_FILE_FUNCTIONS, true)) {
throw new AssertionError("An error occurred while analyzing plugin $class: for performance reasons, plugins may not use the file function $name, please use properties and __sleep to store plugin-related configuration in the session!");
}
}
/** @var New_ $call */
foreach ($finder->findInstanceOf($file, New_::class) as $new) {
if ($new->class instanceof Name
&& \in_array($name = $new->class->toLowerString(), self::BANNED_CLASSES, true)
) {
throw new AssertionError("An error occurred while analyzing plugin $class: for performance reasons, plugins may not use the non-async blocking class $name!");
}
}
/** @var Include_ $include */
$include = $finder->findFirstInstanceOf($file, Include_::class);
if ($include
&& !($include->expr instanceof String_ && \in_array($include->expr->value, ['vendor/autoload.php', 'madeline.php', 'madeline.phar'], true))
) {
throw new AssertionError("An error occurred while analyzing plugin $class: for performance reasons, plugins can only automatically include or require other files present in the plugins folder by triggering the PSR-4 autoloader (not by manually require()'ing them).");
}
}
}

View File

@ -1603,7 +1603,7 @@ final class MTProto implements TLCallback, LoggerGetter
}
if ($this->event_handler_instance instanceof EventHandler) {
try {
EventHandler::validateEventHandler($this->event_handler_instance::class);
Tools::validateEventHandlerClass($this->event_handler_instance::class);
} catch (AssertionError $e) {
Logger::log($e->getMessage(), Logger::FATAL_ERROR);
$e = \htmlentities($e->getMessage());

View File

@ -28,7 +28,7 @@ abstract class PluginEventHandler extends SimpleEventHandler
/**
* Plugins can require other plugins ONLY with the getPlugins() method.
*/
final public function getPluginPaths(): string|array|null
final public static function getPluginPaths(): string|array|null
{
return null;
}

View File

@ -22,11 +22,23 @@ namespace danog\MadelineProto;
use Amp\ByteStream\ReadableBuffer;
use ArrayAccess;
use AssertionError;
use Closure;
use Countable;
use Exception;
use Fiber;
use PhpParser\Node\Name;
use PhpParser\Node\Scalar\LNumber;
use PhpParser\Node\Scalar\String_;
use PhpParser\Node\Stmt\Class_;
use PhpParser\Node\Stmt\ClassLike;
use PhpParser\Node\Stmt\DeclareDeclare;
use PhpParser\NodeFinder;
use PhpParser\NodeVisitor\NameResolver;
use PhpParser\ParserFactory;
use phpseclib3\Crypt\Random;
use PHPStan\PhpDocParser\Ast\NodeTraverser;
use ReflectionClass;
use Throwable;
use Traversable;
use Webmozart\Assert\Assert;
@ -36,6 +48,8 @@ use const DIRECTORY_SEPARATOR;
use const PHP_INT_MAX;
use const PHP_SAPI;
use const STR_PAD_RIGHT;
use function Amp\File\read;
use function unpack;
/**
@ -567,4 +581,102 @@ abstract class Tools extends AsyncTools
}
return null;
}
/**
* Perform static analysis on a certain event handler class, to make sure it satisfies some performance requirements.
*
* @param class-string<EventHandler> $class Class name
*
* @throws AssertionError If validation fails.
*/
public static function validateEventHandlerClass(string $class): void
{
$file = read((new ReflectionClass($class))->getFileName());
self::validateEventHandlerCode($file);
}
private const BANNED_FUNCTIONS = [
'file_get_contents',
'file_put_contents',
'unlink',
'curl_exec',
'mysqli_query',
'mysqli_connect',
'mysql_connect',
'fopen',
'fsockopen',
];
private const BANNED_FILE_FUNCTIONS = [
'amp\\file\\read',
'amp\\file\\write',
'amp\\file\\get',
'amp\\file\\put',
];
private const BANNED_CLASSES = [
PDO::class,
mysqli::class,
];
/**
* Perform static analysis on a certain event handler class, to make sure it satisfies some performance requirements.
*
* @param string $code Code of the class.
*
* @throws AssertionError If validation fails.
*/
public static function validateEventHandlerCode(string $code): void
{
$code = (new ParserFactory)->create(ParserFactory::ONLY_PHP7)->parse($code);
Assert::notNull($code);
$traverser = new NodeTraverser([new NameResolver()]);
$code = $traverser->traverse($code);
$finder = new NodeFinder;
$class = $finder->findInstanceOf($code, ClassLike::class);
$class = \array_filter($class, fn (ClassLike $c): bool => $c->name !== null);
if (\count($class) !== 1 || !$class[0] instanceof Class_) {
throw new AssertionError("A file must define exactly one class! To define multiple classes, interfaces or traits, create separate files, they will be autoloaded by MadelineProto automatically.");
}
$class = $class[0]->name->toString();
/** @var DeclareDeclare|null $call */
$declare = $finder->findFirstInstanceOf($code, DeclareDeclare::class);
if ($declare === null
|| $declare->key->name !== 'strict_types'
|| !$declare->value instanceof LNumber
|| $declare->value->value !== 1
) {
throw new AssertionError("An error occurred while analyzing plugin $class: for performance reasons, the first statement of a plugin must be declare(strict_types=1);");
}
/** @var FuncCall $call */
foreach ($finder->findInstanceOf($code, FuncCall::class) as $call) {
if (!$call->name instanceof Name) {
continue;
}
$name = $call->name->toLowerString();
if (\in_array($name, self::BANNED_FUNCTIONS, true)) {
throw new AssertionError("An error occurred while analyzing plugin $class: for performance reasons, plugins may not use the non-async blocking function $name!");
}
if (\in_array($name, self::BANNED_FILE_FUNCTIONS, true)) {
throw new AssertionError("An error occurred while analyzing plugin $class: for performance reasons, plugins may not use the file function $name, please use properties and __sleep to store plugin-related configuration in the session!");
}
}
/** @var New_ $call */
foreach ($finder->findInstanceOf($code, New_::class) as $new) {
if ($new->class instanceof Name
&& \in_array($name = $new->class->toLowerString(), self::BANNED_CLASSES, true)
) {
throw new AssertionError("An error occurred while analyzing plugin $class: for performance reasons, plugins may not use the non-async blocking class $name!");
}
}
/** @var Include_ $include */
$include = $finder->findFirstInstanceOf($code, Include_::class);
if ($include
&& !($include->expr instanceof String_ && \in_array($include->expr->value, ['vendor/autoload.php', 'madeline.php', 'madeline.phar'], true))
) {
throw new AssertionError("An error occurred while analyzing plugin $class: for performance reasons, plugins can only automatically include or require other files present in the plugins folder by triggering the PSR-4 autoloader (not by manually require()'ing them).");
}
}
}