Created
February 23, 2024 13:18
-
-
Save lostfocus/a6202e2aa8cd8073e686b2c97b955595 to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<?php | |
declare(strict_types=1); | |
namespace App\Service\ActivityPub; | |
use App\Entity\User; | |
use JsonException; | |
use Psr\Log\LoggerInterface; | |
use RuntimeException; | |
use Symfony\Contracts\HttpClient\Exception\ClientExceptionInterface; | |
use Symfony\Contracts\HttpClient\Exception\RedirectionExceptionInterface; | |
use Symfony\Contracts\HttpClient\Exception\ServerExceptionInterface; | |
use Symfony\Contracts\HttpClient\Exception\TransportExceptionInterface; | |
use Symfony\Contracts\HttpClient\HttpClientInterface; | |
use function base64_encode; | |
use function gmdate; | |
use function hash; | |
use function openssl_sign; | |
use function sprintf; | |
use const OPENSSL_ALGO_SHA256; | |
readonly class Client | |
{ | |
public function __construct( | |
private HttpClientInterface $httpClient, | |
private LoggerInterface $activitypubLogger | |
) { | |
} | |
/** | |
* @param string $inboxUrl | |
* @param array<string, mixed> $activity | |
* @param User|null $user | |
* @return void | |
* @throws ClientExceptionInterface | |
* @throws JsonException | |
* @throws RedirectionExceptionInterface | |
* @throws ServerExceptionInterface | |
* @throws TransportExceptionInterface | |
*/ | |
public function sendToInbox(string $inboxUrl, array $activity, ?User $user = null): void | |
{ | |
if ($user === null) { | |
$this->activitypubLogger->debug('No user given', [ | |
'inboxUrl' => $inboxUrl, | |
'message' => $activity, | |
]); | |
return; | |
} | |
$body = json_encode($activity, JSON_THROW_ON_ERROR); | |
$date = gmdate('D, d M Y H:i:s T'); | |
$digest = $this->generateDigest($body); | |
$signature = $this->generateSignature($user, $inboxUrl, $date, $digest); | |
$this->activitypubLogger->debug(__FUNCTION__, [ | |
'inboxUrl' => $inboxUrl, | |
'message' => $activity, | |
'Accept' => 'application/activity+json,application/ld+json,application/json', | |
'Content-Type' => 'application/activity+json', | |
'Digest' => "SHA-256=$digest", | |
'Signature' => $signature, | |
'Date' => $date, | |
]); | |
try { | |
$response = $this->httpClient->request('POST', $inboxUrl, [ | |
'body' => $body, | |
'headers' => [ | |
'Accept' => 'application/activity+json,application/ld+json,application/json', | |
'Content-Type' => 'application/activity+json', | |
'Digest' => "SHA-256=$digest", | |
'Signature' => $signature, | |
'Date' => $date, | |
], | |
]); | |
} catch (TransportExceptionInterface $e) { | |
$this->activitypubLogger->error($e->getMessage(), [ | |
'type' => $e::class, | |
'message' => $e->getMessage(), | |
'code' => $e->getCode(), | |
]); | |
throw $e; | |
} | |
try { | |
$content = $response->getContent(); | |
} catch (ClientExceptionInterface|RedirectionExceptionInterface|ServerExceptionInterface|TransportExceptionInterface $e) { | |
/** @noinspection PhpUnhandledExceptionInspection */ | |
$content = $response->getContent(false); | |
$this->activitypubLogger->error($e->getMessage(), [ | |
'type' => $e::class, | |
'message' => $e->getMessage(), | |
'code' => $e->getCode(), | |
'content' => $content, | |
]); | |
throw $e; | |
} | |
$this->activitypubLogger->debug('response', [ | |
'content' => $content, | |
'code' => $response->getStatusCode(), | |
]); | |
} | |
private function generateDigest(string $body): string | |
{ | |
return base64_encode(hash('sha256', $body, true)); | |
} | |
private function generateSignature(User $user, string $inboxUrl, string $date, string $digest, string $method = 'post'): string | |
{ | |
if ($user->getPrivateKey() === null) { | |
throw new RuntimeException(); | |
} | |
$urlParts = parse_url($inboxUrl); | |
if (!is_array($urlParts)) { | |
throw new RuntimeException(); | |
} | |
if (!array_key_exists('host', $urlParts) || !array_key_exists('path', $urlParts)) { | |
throw new RuntimeException(); | |
} | |
$host = $urlParts['host']; | |
$path = $urlParts['path']; | |
if (array_key_exists('query', $urlParts)) { | |
$query = $urlParts['query']; | |
if (trim($query) !== '') { | |
$path .= '?'.$query; | |
} | |
} | |
$signed_string = sprintf("(request-target): %s %s\nhost: %s\ndate: %s\ndigest: SHA-256=%s", $method, $path, $host, $date, $digest); | |
openssl_sign($signed_string, $signature, $user->getPrivateKey(), OPENSSL_ALGO_SHA256); | |
$signature = base64_encode($signature); // phpcs:ignore | |
$key_id = $user->getProfileUrl().'#main-key'; | |
return sprintf('keyId="%s",algorithm="rsa-sha256",headers="(request-target) host date digest",signature="%s"', $key_id, $signature); | |
} | |
/** | |
* @param string $webfingerUrl | |
* @return array<string, mixed> | |
* @throws ClientExceptionInterface | |
* @throws JsonException | |
* @throws RedirectionExceptionInterface | |
* @throws ServerExceptionInterface | |
* @throws TransportExceptionInterface | |
*/ | |
public function webfinger(string $webfingerUrl): array | |
{ | |
$webfingerResult = $this->httpClient->request('GET', $webfingerUrl, [ | |
'headers' => [ | |
'Accept' => 'application/json', | |
], | |
]); | |
$content = json_decode($webfingerResult->getContent(), true, 512, JSON_THROW_ON_ERROR); | |
if (is_array($content)) { | |
return $content; | |
} | |
return []; | |
} | |
/** | |
* @return array<string, mixed> | |
* @throws ClientExceptionInterface | |
* @throws JsonException | |
* @throws RedirectionExceptionInterface | |
* @throws ServerExceptionInterface | |
* @throws TransportExceptionInterface | |
*/ | |
public function load(string $url, ?User $user = null): array | |
{ | |
$headers = [ | |
'Accept' => 'application/activity+json,application/ld+json,application/json', | |
]; | |
if ($user !== null) { | |
$date = gmdate('D, d M Y H:i:s T'); | |
$digest = $this->generateDigest(''); | |
$signature = $this->generateSignature($user, $url, $date, $digest, 'get'); | |
$headers['Digest'] = 'SHA-256='.$digest; | |
$headers['Signature'] = $signature; | |
$headers['Date'] = $date; | |
} | |
$result = $this->httpClient->request('GET', $url, [ | |
'headers' => $headers, | |
]); | |
$content = json_decode($result->getContent(), true, 512, JSON_THROW_ON_ERROR); | |
if (is_array($content)) { | |
return $content; | |
} | |
return []; | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
https://gist.github.com/lostfocus/a6202e2aa8cd8073e686b2c97b955595#file-client-php-L151 should be: