Skip to content

Commit

Permalink
Include plugins before deserializing session
Browse files Browse the repository at this point in the history
  • Loading branch information
danog committed Jul 13, 2023
1 parent 3e8047c commit b9a9aa8
Show file tree
Hide file tree
Showing 5 changed files with 131 additions and 105 deletions.
5 changes: 5 additions & 0 deletions examples/bot.php
Original file line number Diff line number Diff line change
Expand Up @@ -186,6 +186,11 @@ public function pingCommand(Incoming&Message $message): void
{
$message->reply('pong');
}

public static function getPluginPaths(): string|array|null
{
return 'plugins/';
}
}

$settings = new Settings;
Expand Down
115 changes: 12 additions & 103 deletions src/EventHandler.php
Original file line number Diff line number Diff line change
Expand Up @@ -35,19 +35,7 @@
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;
Expand All @@ -57,7 +45,6 @@
use function Amp\File\isDirectory;
use function Amp\File\isFile;
use function Amp\File\listFiles;
use function Amp\File\read;

/**
* Event handler.
Expand All @@ -81,6 +68,7 @@ final public static function startAndLoop(string $session, ?SettingsAbstract $se
if (self::$includingPlugins) {
return;
}
static::internalGetDirectoryPlugins();
$settings ??= new SettingsEmpty;
$API = new API($session, $settings);
$API->startAndLoopInternal(static::class);
Expand All @@ -99,6 +87,7 @@ final public static function startAndLoopBot(string $session, string $token, ?Se
if (self::$includingPlugins) {
return;
}
static::internalGetDirectoryPlugins();
$settings ??= new SettingsEmpty;
$API = new API($session, $settings);
$API->botLogin($token);
Expand Down Expand Up @@ -221,7 +210,7 @@ function (PeriodicLoop $loop) use ($closure): bool {
};
}
if ($this instanceof SimpleEventHandler) {
self::validateEventHandler(static::class);
Tools::validateEventHandlerClass(static::class);
}
if ($has_any) {
$onAny = $this->onAny(...);
Expand Down Expand Up @@ -290,7 +279,7 @@ public function getReportPeers()
*
* @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;
}
Expand All @@ -299,7 +288,7 @@ public function getPluginPaths(): string|array|null
*
* @return array<class-string<EventHandler>>
*/
public function getPlugins(): array
public static function getPlugins(): array
{
return [];
}
Expand All @@ -309,31 +298,31 @@ public function getPlugins(): array
*
* @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) {
Expand Down Expand Up @@ -415,84 +404,4 @@ private function internalGetDirectoryPlugins(): array

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).");
}
}
}
2 changes: 1 addition & 1 deletion src/MTProto.php
Original file line number Diff line number Diff line change
Expand Up @@ -1603,7 +1603,7 @@ public function getWebMessage(string $message): string
}
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());
Expand Down
2 changes: 1 addition & 1 deletion src/PluginEventHandler.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
Expand Down
112 changes: 112 additions & 0 deletions src/Tools.php
Original file line number Diff line number Diff line change
Expand Up @@ -22,11 +22,23 @@

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;
Expand All @@ -36,6 +48,8 @@
use const PHP_INT_MAX;
use const PHP_SAPI;
use const STR_PAD_RIGHT;

use function Amp\File\read;
use function unpack;

/**
Expand Down Expand Up @@ -567,4 +581,102 @@ public static function parseLink(string $link): array|null
}
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).");
}
}
}

0 comments on commit b9a9aa8

Please sign in to comment.