Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@
"league/oauth2-github": "^3.1.0",
"league/oauth2-google": "^4.0.1",
"league/oauth2-server-bundle": "^1.0.0",
"league/uri-interfaces": "^7.8",
"liip/imagine-bundle": "^2.13.1",
"meteo-concept/hcaptcha-bundle": "^4.1.0",
"minishlink/web-push": "^10.0.3",
Expand Down
4 changes: 2 additions & 2 deletions composer.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

6 changes: 5 additions & 1 deletion src/Command/Update/Async/NoteVisibilityHandler.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
namespace App\Command\Update\Async;

use App\Entity\Contracts\VisibilityInterface;
use App\Factory\WwwHttpClientFactory;
use Doctrine\ORM\EntityManagerInterface;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Messenger\Attribute\AsMessageHandler;
Expand All @@ -13,10 +14,13 @@
#[AsMessageHandler]
readonly class NoteVisibilityHandler
{
private HttpClientInterface $client;

public function __construct(
private EntityManagerInterface $entityManager,
private HttpClientInterface $client,
WwwHttpClientFactory $clientFactory,
) {
$this->client = $clientFactory->getClient();
}

public function __invoke(NoteVisibilityMessage $message): void
Expand Down
10 changes: 9 additions & 1 deletion src/Controller/AjaxController.php
Original file line number Diff line number Diff line change
Expand Up @@ -27,12 +27,14 @@
use App\Service\SettingsManager;
use App\Service\UserNoteManager;
use App\Utils\Embed;
use App\Utils\UrlUtils;
use Doctrine\DBAL\ParameterType;
use Doctrine\ORM\EntityManagerInterface;
use Psr\Log\LoggerInterface;
use Symfony\Bridge\Doctrine\Attribute\MapEntity;
use Symfony\Bundle\SecurityBundle\Security;
use Symfony\Component\Emoji\EmojiTransliterator;
use Symfony\Component\HttpFoundation\Exception\BadRequestException;
use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpKernel\Attribute\MapQueryParameter;
Expand Down Expand Up @@ -207,7 +209,13 @@ public function fetchOnline(
HttpClientInterface $httpClient,
CacheInterface $cache,
): JsonResponse {
$resp = $httpClient->request('GET', $mercurePublicUrl.'/subscriptions/'.$topic, [
$mercureSubsUrl = $mercurePublicUrl.'/subscriptions/';
$mercureUrl = $mercureSubsUrl.$topic;
if (!UrlUtils::checkUrlSubpathNotAscending($mercureSubsUrl, $topic)) {
throw new BadRequestException('Mercure topic is malformed');
}

$resp = $httpClient->request('GET', $mercureUrl, [
'auth_bearer' => $mercureSubscriptionsToken,
]);

Expand Down
80 changes: 80 additions & 0 deletions src/Factory/WwwHttpClientFactory.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
<?php
declare(strict_types=1);

namespace App\Factory;

use Embed\Http\Crawler;
use Symfony\Component\HttpClient\NoPrivateNetworkHttpClient;
use Symfony\Component\HttpClient\Psr18Client;
use Symfony\Contracts\HttpClient\HttpClientInterface;
use Symfony\Contracts\Service\ResetInterface;

/**
* Creates HttpClients which block requests to non-WWW (e.g. localhost and LAN) destinations.
*/
class WwwHttpClientFactory implements ResetInterface
{
private readonly HttpClientInterface $defaultClient;
private array $existingClients;

public function __construct(
private readonly HttpClientInterface $httpClientBase,
) {
$this->existingClients = [];
$this->defaultClient = $this->buildFilteredClient($this->buildConfiguredClient($this->httpClientBase));
Comment thread
melroy89 marked this conversation as resolved.
}

public function getClient(?HttpClientInterface $base = null, ?array $clientOptions = null): HttpClientInterface
{
if (null === $base && null === $clientOptions) {
return $this->defaultClient;
} elseif (null === $clientOptions) {
return $this->buildFilteredClient($this->buildConfiguredClient($base ?? $this->httpClientBase));
} else {
return $this->buildFilteredClient(($base ?? $this->httpClientBase)->withOptions($clientOptions));
}
}

public function getPsr18Client(?HttpClientInterface $base = null, ?array $clientOptions = null): Psr18Client
{
$client = $this->getClient($base, $clientOptions);
return new Psr18Client($client);
}

public function getEmbedCrawler(?HttpClientInterface $base = null, ?array $clientOptions = null): Crawler
{
$client = $this->getPsr18Client($base, $clientOptions);
return new Crawler($client, $client, $client);
}

private function buildConfiguredClient(HttpClientInterface $client): HttpClientInterface
{
return $client->withOptions([
'max_redirects' => 10,
'max_duration' => 10,
'timeout' => 10,
]);
}

public function reset(): void
{
$stillExistingClients = [];
foreach ($this->existingClients as $clientRef) {
/** @var \WeakReference<NoPrivateNetworkHttpClient> $clientRef */
$client = $clientRef->get();
if (null !== $client) {
$client->reset();
$stillExistingClients[] = $clientRef;
}
}

$this->existingClients = $stillExistingClients;
}

private function buildFilteredClient(HttpClientInterface $client): HttpClientInterface
{
$ret = new NoPrivateNetworkHttpClient($client);
$this->existingClients[] = \WeakReference::create($ret);
return $ret;
}
}
14 changes: 8 additions & 6 deletions src/Service/ActivityPub/ApHttpClient.php
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
use App\Factory\ActivityPub\GroupFactory;
use App\Factory\ActivityPub\PersonFactory;
use App\Factory\ActivityPub\TombstoneFactory;
use App\Factory\WwwHttpClientFactory;
use App\Repository\MagazineRepository;
use App\Repository\SiteRepository;
use App\Repository\UserRepository;
Expand Down Expand Up @@ -59,6 +60,7 @@ public function __construct(
private readonly TombstoneFactory $tombstoneFactory,
private readonly PersonFactory $personFactory,
private readonly GroupFactory $groupFactory,
private readonly WwwHttpClientFactory $httpClientFactory,
private readonly LoggerInterface $logger,
private readonly CacheInterface $cache,
private readonly UserRepository $userRepository,
Expand Down Expand Up @@ -119,7 +121,7 @@ private function getActivityObjectImpl(string $url): ?string
}

try {
$client = new CurlHttpClient();
$client = $this->httpClientFactory->getClient(new CurlHttpClient());
$response = $client->request('GET', $url, [
'max_duration' => self::MAX_DURATION,
'timeout' => self::TIMEOUT,
Expand Down Expand Up @@ -209,7 +211,7 @@ private function getWebfingerObjectImpl(string $url): ?string
} catch (\Throwable) {
}
try {
$client = new CurlHttpClient();
$client = $this->httpClientFactory->getClient(new CurlHttpClient());
$response = $client->request('GET', $url, [
'max_duration' => self::MAX_DURATION,
'timeout' => self::TIMEOUT,
Expand Down Expand Up @@ -275,7 +277,7 @@ private function getActorObjectImpl(string $apProfileId): ?string

try {
// Set-up request
$client = new CurlHttpClient();
$client = $this->httpClientFactory->getClient(new CurlHttpClient());
$response = $client->request('GET', $apProfileId, [
'max_duration' => self::MAX_DURATION,
'timeout' => self::TIMEOUT,
Expand Down Expand Up @@ -382,7 +384,7 @@ private function getCollectionObjectImpl(string $apAddress): ?string

try {
// Set-up request
$client = new CurlHttpClient();
$client = $this->httpClientFactory->getClient(new CurlHttpClient());
$response = $client->request('GET', $apAddress, [
'max_duration' => self::MAX_DURATION,
'timeout' => self::TIMEOUT,
Expand Down Expand Up @@ -489,7 +491,7 @@ public function post(string $url, User|Magazine $actor, ?array $body = null, boo

// Set-up request
try {
$client = new CurlHttpClient();
$client = $this->httpClientFactory->getClient(new CurlHttpClient());
$response = $client->request('POST', $url, [
'max_duration' => self::MAX_DURATION,
'timeout' => self::TIMEOUT,
Expand Down Expand Up @@ -549,7 +551,7 @@ public function fetchInstanceNodeInfo(string $url, bool $decoded = true): array|
*/
private function generalFetch(string $url, ApRequestType $requestType = ApRequestType::ActivityPub): string
{
$client = new CurlHttpClient();
$client = $this->httpClientFactory->getClient(new CurlHttpClient());
$this->logger->debug("[ApHttpClient::generalFetch] URL: $url");
$r = $client->request('GET', $url, [
'max_duration' => self::MAX_DURATION,
Expand Down
6 changes: 5 additions & 1 deletion src/Service/ImageManager.php
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
use App\Entity\Image as MbinImage;
use App\Exception\CorruptedFileException;
use App\Exception\ImageDownloadTooLargeException;
use App\Factory\WwwHttpClientFactory;
use App\Repository\ImageRepository;
use App\Twig\Runtime\FormattingExtensionRuntime;
use App\Utils\GeneralUtil;
Expand All @@ -33,10 +34,11 @@ class ImageManager implements ImageManagerInterface
];
public const string IMAGE_MIMETYPE_STR = 'image/jpeg, image/jpg, image/gif, image/png, image/jxl, image/heic, image/heif, image/webp, image/avif';

private readonly HttpClientInterface $httpClient;

public function __construct(
private readonly string $storageUrl,
private readonly FilesystemOperator $publicUploadsFilesystem,
private readonly HttpClientInterface $httpClient,
private readonly MimeTypesInterface $mimeTypeGuesser,
private readonly ValidatorInterface $validator,
private readonly LoggerInterface $logger,
Expand All @@ -45,7 +47,9 @@ public function __construct(
private readonly float $imageCompressionQuality,
private readonly CacheManager $imagineCacheManager,
private readonly EntityManagerInterface $entityManager,
WwwHttpClientFactory $httpClientFactory,
) {
$this->httpClient = $httpClientFactory->getClient();
}

public static function isImageUrl(string $url): bool
Expand Down
19 changes: 4 additions & 15 deletions src/Utils/Embed.php
Original file line number Diff line number Diff line change
Expand Up @@ -7,19 +7,16 @@
use App\Entity\Entry;
use App\Event\ActivityPub\CurlRequestBeginningEvent;
use App\Event\ActivityPub\CurlRequestFinishedEvent;
use App\Factory\WwwHttpClientFactory;
use App\Service\ImageManager;
use App\Service\SettingsManager;
use App\Service\VideoManager;
use Embed\Embed as BaseEmbed;
use Embed\Extractor;
use Embed\Http\Crawler;
use Psr\Log\LoggerInterface;
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
use Symfony\Component\HttpClient\NoPrivateNetworkHttpClient;
use Symfony\Component\HttpClient\Psr18Client;
use Symfony\Contracts\Cache\CacheInterface;
use Symfony\Contracts\Cache\ItemInterface;
use Symfony\Contracts\HttpClient\HttpClientInterface;

class Embed
{
Expand All @@ -34,7 +31,7 @@ public function __construct(
private SettingsManager $settings,
private LoggerInterface $logger,
private EventDispatcherInterface $dispatcher,
private HttpClientInterface $httpClient,
private WwwHttpClientFactory $httpClientFactory,
) {
}

Expand All @@ -44,7 +41,7 @@ public function __clone(): void
unset($this->settings);
unset($this->logger);
unset($this->dispatcher);
unset($this->httpClient);
unset($this->httpClientFactory);
}

public function fetch(string $url): self
Expand Down Expand Up @@ -114,15 +111,7 @@ function (ItemInterface $item) use ($url) {

private function fetchEmbed(string $url): Extractor
{
$httpClient = new NoPrivateNetworkHttpClient(
$this->httpClient->withOptions([
'max_redirects' => 10,
'max_duration' => 10,
'timeout' => 10,
])
);
$psr18Client = new Psr18Client($httpClient);
$fetcher = new BaseEmbed(new Crawler($psr18Client, $psr18Client, $psr18Client));
$fetcher = new BaseEmbed($this->httpClientFactory->getEmbedCrawler());
$embed = $fetcher->get($url);

if ($this->detectFaultyRedirectEmbed($embed)) {
Expand Down
16 changes: 16 additions & 0 deletions src/Utils/UrlUtils.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

namespace App\Utils;

use League\Uri\UriString;
use Symfony\Component\HttpFoundation\Request;

class UrlUtils
Expand Down Expand Up @@ -52,4 +53,19 @@ public static function extractUrlsFromString(string $text): array

return $urls;
}

/**
* Checks that a given sub-path will not change the absolute URL path of the url to a different parent when appended to the base URL.
* An example for an ascending sub-path would be: baseUrl = https://example.com/parent/ subPath = ../otherParent/child.
*
* @param string $baseUrl The URL where the sub-path will be appended to. Must end with a '/'.
* @param string $subPath the relative URL to be appended to $baseUrl
*/
public static function checkUrlSubpathNotAscending(string $baseUrl, string $subPath): bool
{
$baseUri = UriString::normalize($baseUrl);
$absoluteUri = UriString::normalize(UriString::resolve($subPath, $baseUri));

return str_starts_with($absoluteUri, $baseUri);
}
}
6 changes: 3 additions & 3 deletions tests/Service/TestingImageManager.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
namespace App\Tests\Service;

use App\Entity\Image;
use App\Factory\WwwHttpClientFactory;
use App\Repository\ImageRepository;
use App\Service\ImageManager;
use App\Service\ImageManagerInterface;
Expand All @@ -17,7 +18,6 @@
use Symfony\Component\DependencyInjection\Attribute\When;
use Symfony\Component\Mime\MimeTypesInterface;
use Symfony\Component\Validator\Validator\ValidatorInterface;
use Symfony\Contracts\HttpClient\HttpClientInterface;

#[When(env: 'test')]
class TestingImageManager implements ImageManagerInterface
Expand All @@ -28,7 +28,6 @@ class TestingImageManager implements ImageManagerInterface
public function __construct(
string $storageUrl,
FilesystemOperator $publicUploadsFilesystem,
HttpClientInterface $httpClient,
MimeTypesInterface $mimeTypeGuesser,
ValidatorInterface $validator,
LoggerInterface $logger,
Expand All @@ -37,8 +36,9 @@ public function __construct(
float $imageCompressionQuality,
CacheManager $imagineCacheManager,
EntityManagerInterface $entityManager,
WwwHttpClientFactory $wwwHttpClientFactory,
) {
$this->innerImageManager = new ImageManager($storageUrl, $publicUploadsFilesystem, $httpClient, $mimeTypeGuesser, $validator, $logger, $settings, $formattingExtensionRuntime, $imageCompressionQuality, $imagineCacheManager, $entityManager);
$this->innerImageManager = new ImageManager($storageUrl, $publicUploadsFilesystem, $mimeTypeGuesser, $validator, $logger, $settings, $formattingExtensionRuntime, $imageCompressionQuality, $imagineCacheManager, $entityManager, $wwwHttpClientFactory);
}

public function setKibbyPath(string $kibbyPath): void
Expand Down
Loading
Loading