Skip to content

Commit

Permalink
Add GET /.well-known/webfinger (#2459)
Browse files Browse the repository at this point in the history
  • Loading branch information
chitoku-k authored Nov 18, 2023
1 parent 5104cce commit df2bae8
Show file tree
Hide file tree
Showing 8 changed files with 261 additions and 2 deletions.
9 changes: 9 additions & 0 deletions api/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,20 @@ API リファレンス

## 目次

- [Web Finger API](#web-finger-api)
- [Activity Streams API](#activity-streams-api)
- [Check API](#check-api)
- [List API](#list-api)
- [Badge API](#badge-api)

## Web Finger API

```
/.well-known/webfinger?resource={resource}
```

指定されたリソースの情報を [Web Finger](https://datatracker.ietf.org/doc/html/rfc7033) 形式で返します。

## Activity Streams API

```
Expand Down
30 changes: 30 additions & 0 deletions api/src/Action/WebFingerAction.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
<?php
declare(strict_types=1);

namespace HomoChecker\Action;

use HomoChecker\Contracts\Service\ActivityPubService;
use Slim\Http\Response;
use Slim\Http\ServerRequest as Request;

class WebFingerAction
{
public function __construct(protected ActivityPubService $activityPub) {}

public function __invoke(Request $request, Response $response)
{
$resource = $request->getQueryParams()['resource'] ?? null;
if (!$resource) {
return $response->withStatus(400);
}

$webFinger = $this->activityPub->webFinger($resource);
if (!$webFinger) {
return $response->withStatus(404);
}

return $response
->withJson($webFinger)
->withHeader('Content-Type', 'application/jrd+json; charset=utf-8');
}
}
5 changes: 5 additions & 0 deletions api/src/Contracts/Service/ActivityPubService.php
Original file line number Diff line number Diff line change
Expand Up @@ -10,4 +10,9 @@ interface ActivityPubService
* @return array The actor object that describes this application.
*/
public function actor(): array;

/**
* Retrieves the Web Finger of the given resource.
*/
public function webFinger(string $resource): null|array;
}
3 changes: 3 additions & 0 deletions api/src/Providers/HomoAppProvider.php
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
use HomoChecker\Action\HealthCheckAction;
use HomoChecker\Action\ListAction;
use HomoChecker\Action\MetricsAction;
use HomoChecker\Action\WebFingerAction;
use HomoChecker\Contracts\Repository\HomoRepository as HomoRepositoryContract;
use HomoChecker\Contracts\Repository\ProfileRepository as ProfileRepositoryContract;
use HomoChecker\Http\NonBufferedBody;
Expand All @@ -28,6 +29,7 @@ class HomoAppProvider extends ServiceProvider
{
public function register()
{
$this->app->singleton(WebFingerAction::class);
$this->app->singleton(ActivityPubActorAction::class);
$this->app->singleton(HealthCheckAction::class);
$this->app->singleton(MetricsAction::class);
Expand All @@ -45,6 +47,7 @@ public function register()
$this->app->singleton('app', function (Container $app) {
AppFactory::setContainer($app);
$slim = AppFactory::create();
$slim->get('/.well-known/webfinger', WebFingerAction::class);
$slim->get('/actor', ActivityPubActorAction::class);
$slim->get('/healthz', HealthCheckAction::class);
$slim->get('/metrics', MetricsAction::class);
Expand Down
24 changes: 24 additions & 0 deletions api/src/Service/ActivityPubService.php
Original file line number Diff line number Diff line change
Expand Up @@ -33,4 +33,28 @@ public function actor(): array
],
];
}

/**
* {@inheritdoc}
*/
public function webFinger(string $resource): null|array
{
$domain = \parse_url($this->id, \PHP_URL_HOST);
$acct = "acct:{$this->preferredUsername}@{$domain}";

if ($resource === $acct || $resource === $this->id) {
return [
'subject' => $acct,
'links' => [
[
'rel' => 'self',
'type' => 'application/activity+json',
'href' => $this->id,
],
],
];
}

return null;
}
}
142 changes: 142 additions & 0 deletions api/tests/Case/Action/WebFingerActionTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,142 @@
<?php
declare(strict_types=1);

namespace HomoChecker\Test\Action;

use HomoChecker\Action\WebFingerAction;
use HomoChecker\Contracts\Service\ActivityPubService;
use Mockery\Adapter\Phpunit\MockeryPHPUnitIntegration;
use Mockery as m;
use Mockery\MockInterface;
use PHPUnit\Framework\TestCase;
use Slim\Http\Response as HttpResponse;
use Slim\Http\ServerRequest as HttpRequest;
use Slim\Psr7\Factory\RequestFactory;
use Slim\Psr7\Factory\StreamFactory;
use Slim\Psr7\Response;

class WebFingerActionTest extends TestCase
{
use MockeryPHPUnitIntegration;

public function testInstanceActor(): void
{
$request = (new RequestFactory())->createRequest('GET', '/.well-known/webfinger?resource=acct:[email protected]');

/** @var ActivityPubService&MockInterface $activityPub */
$activityPub = m::mock(ActivityPubService::class);
$activityPub->shouldReceive('webFinger')
->with('acct:[email protected]')
->andReturn([
'subject' => 'acct:[email protected]',
'links' => [
[
'rel' => 'self',
'type' => 'application/activity+json',
'href' => 'https://example.com/actor',
],
],
]);

$action = new WebFingerAction($activityPub);
$response = $action(new HttpRequest($request), new HttpResponse(new Response(), new StreamFactory()), []);

$actual = $response->getHeaderLine('Content-Type');
$this->assertMatchesRegularExpression('|^application/jrd\+json|', $actual);

$actual = $response->getStatusCode();
$this->assertEquals(200, $actual);

$actual = (string) $response->getBody();
$expected = json_encode([
'subject' => 'acct:[email protected]',
'links' => [
[
'rel' => 'self',
'type' => 'application/activity+json',
'href' => 'https://example.com/actor',
],
],
]);
$this->assertJsonStringEqualsJsonString($expected, $actual);
}

public function testInstanceActorID(): void
{
$request = (new RequestFactory())->createRequest('GET', '/.well-known/webfinger?resource=https://example.com/actor');

/** @var ActivityPubService&MockInterface $activityPub */
$activityPub = m::mock(ActivityPubService::class);
$activityPub->shouldReceive('webFinger')
->with('https://example.com/actor')
->andReturn([
'subject' => 'acct:[email protected]',
'links' => [
[
'rel' => 'self',
'type' => 'application/activity+json',
'href' => 'https://example.com/actor',
],
],
]);

$action = new WebFingerAction($activityPub);
$response = $action(new HttpRequest($request), new HttpResponse(new Response(), new StreamFactory()), []);

$actual = $response->getHeaderLine('Content-Type');
$this->assertMatchesRegularExpression('|^application/jrd\+json|', $actual);

$actual = $response->getStatusCode();
$this->assertEquals(200, $actual);

$actual = (string) $response->getBody();
$expected = json_encode([
'subject' => 'acct:[email protected]',
'links' => [
[
'rel' => 'self',
'type' => 'application/activity+json',
'href' => 'https://example.com/actor',
],
],
]);
$this->assertJsonStringEqualsJsonString($expected, $actual);
}

public function testInvalidActor(): void
{
$request = (new RequestFactory())->createRequest('GET', '/.well-known/webfinger');

/** @var ActivityPubService&MockInterface $activityPub */
$activityPub = m::mock(ActivityPubService::class);

$action = new WebFingerAction($activityPub);
$response = $action(new HttpRequest($request), new HttpResponse(new Response(), new StreamFactory()), []);

$actual = $response->getStatusCode();
$this->assertEquals(400, $actual);

$actual = (string) $response->getBody();
$this->assertEquals('', $actual);
}

public function testNonActor(): void
{
$request = (new RequestFactory())->createRequest('GET', '/.well-known/webfinger?resource=acct:[email protected]');

/** @var ActivityPubService&MockInterface $activityPub */
$activityPub = m::mock(ActivityPubService::class);
$activityPub->shouldReceive('webFinger')
->with('acct:[email protected]')
->andReturn(null);

$action = new WebFingerAction($activityPub);
$response = $action(new HttpRequest($request), new HttpResponse(new Response(), new StreamFactory()), []);

$actual = $response->getStatusCode();
$this->assertEquals(404, $actual);

$actual = (string) $response->getBody();
$this->assertEquals('', $actual);
}
}
46 changes: 46 additions & 0 deletions api/tests/Case/Service/ActivityPubServiceTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -43,4 +43,50 @@ public function testActor(): void
$actual = $activityPub->actor();
$this->assertEquals($expected, $actual);
}

public function testInstanceActorWebFinger(): void
{
$activityPub = new ActivityPubService($this->id, $this->preferredUsername, $this->publicKeyPem);

$expected = [
'subject' => 'acct:[email protected]',
'links' => [
[
'rel' => 'self',
'type' => 'application/activity+json',
'href' => 'https://example.com/actor',
],
],
];

$actual = $activityPub->webFinger('acct:[email protected]');
$this->assertEquals($expected, $actual);
}

public function testInstanceActorWebFingerByURL(): void
{
$activityPub = new ActivityPubService($this->id, $this->preferredUsername, $this->publicKeyPem);

$expected = [
'subject' => 'acct:[email protected]',
'links' => [
[
'rel' => 'self',
'type' => 'application/activity+json',
'href' => 'https://example.com/actor',
],
],
];

$actual = $activityPub->webFinger('https://example.com/actor');
$this->assertEquals($expected, $actual);
}

public function testNonActorWebFinger(): void
{
$activityPub = new ActivityPubService($this->id, $this->preferredUsername, $this->publicKeyPem);

$actual = $activityPub->webFinger('acct:[email protected]');
$this->assertNull($actual);
}
}
4 changes: 2 additions & 2 deletions compose.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,8 @@ services:
target: dev
working_dir: /var/www/html/api
environment:
HOMOCHECKER_AP_ACTOR_ID: http://localhost:4545/actor
HOMOCHECKER_AP_ACTOR_PREFERRED_USERNAME: localhost:4545
HOMOCHECKER_AP_ACTOR_ID: http://localhost/actor
HOMOCHECKER_AP_ACTOR_PREFERRED_USERNAME: localhost
HOMOCHECKER_AP_ACTOR_PUBLIC_KEY: /activity_pub_actor_public_key
HOMOCHECKER_DB_HOST: database
HOMOCHECKER_DB_USERNAME: homo
Expand Down

0 comments on commit df2bae8

Please sign in to comment.