Skip to content

Commit

Permalink
Add NIP-42 support for client to relay authentication within the library
Browse files Browse the repository at this point in the history
  • Loading branch information
Sebastix committed Oct 23, 2024
1 parent 889655b commit 10bcf1f
Show file tree
Hide file tree
Showing 9 changed files with 228 additions and 13 deletions.
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -288,7 +288,7 @@ private key on command line.
- [x] `NOTICE` - used to send human-readable messages (like errors) to clients
- [x] Improve handling relay responses
- [ ] Support NIP-19 bech32-encoded identifiers
- [ ] Support NIP-42 authentication of clients to relays => AUTH relay response
- [x] Support NIP-42 authentication of clients to relays
- [ ] Support NIP-45 event counts
- [ ] Support NIP-50 search capability
- [ ] Support multi-threading (async concurrency) for handling requests simultaneously
Expand Down
2 changes: 1 addition & 1 deletion src/Examples/request-events-from-multiple-relays.php
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@
$response = $request->send();

foreach ($response as $relayUrl => $relayResponses) {
print 'Received ' . count($response[$relayUrl]) . ' message(s) found from relay ' . $relayUrl . PHP_EOL;
print 'Received ' . count($response[$relayUrl]) . ' message(s) received from relay ' . $relayUrl . PHP_EOL;
foreach ($relayResponses as $message) {
print $message->event->content . PHP_EOL;
}
Expand Down
53 changes: 53 additions & 0 deletions src/Examples/request-events-with-auth.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
<?php

declare(strict_types=1);

use swentel\nostr\Event\Event;
use swentel\nostr\Filter\Filter;
use swentel\nostr\Message\RequestMessage;
use swentel\nostr\Relay\Relay;
use swentel\nostr\RelayResponse\RelayResponseEvent;
use swentel\nostr\Request\Request;
use swentel\nostr\Subscription\Subscription;

require __DIR__ . '/../../vendor/autoload.php';

try {
$subscription = new Subscription();
$subscriptionId = $subscription->setId();
$filter1 = new Filter();
$filter1->setAuthors([
'npub1qe3e5wrvnsgpggtkytxteaqfprz0rgxr8c3l34kk3a9t7e2l3acslezefe',
]);
$filter1->setKinds([1]);
$filter1->setLimit(3);
$filters = [$filter1];
$requestMessage = new RequestMessage($subscriptionId, $filters);
$relay = new Relay('wss://jingle.nostrver.se');
//$relay = new Relay('wss://hotrightnow.nostr1.com');
$request = new Request($relay, $requestMessage);
$response = $request->send();

foreach ($response as $relay => $messages) {
print 'Received ' . count($response[$relay]) . ' message(s) received from relay ' . $relay . PHP_EOL;
foreach ($messages as $message) {
print $message->type . ': ' . $message->message . PHP_EOL;
if ($message instanceof RelayResponseEvent) {
$rawEvent = $message->event;
$event = new Event();
$event->setId($rawEvent->id);
$event->setPublicKey($rawEvent->pubkey);
$event->setCreatedAt($rawEvent->created_at);
$event->setKind($rawEvent->kind);
$event->setTags($rawEvent->tags);
$event->setContent($rawEvent->content);
$event->setSignature($rawEvent->sig);
if ($event->verify() === true) {
var_dump($event->getContent());
}
}
}
}
} catch (Exception $e) {
print $e->getMessage() . PHP_EOL;
}
6 changes: 4 additions & 2 deletions src/Examples/request-events.php
Original file line number Diff line number Diff line change
Expand Up @@ -31,10 +31,12 @@
* Each message will also contain the event.
*/
foreach ($response as $relayUrl => $relayResponses) {
print 'Received ' . count($response[$relayUrl]) . ' message(s) found from relay ' . $relayUrl . PHP_EOL;
print 'Received ' . count($response[$relayUrl]) . ' message(s) received from relay ' . $relayUrl . PHP_EOL;
/** @var \swentel\nostr\RelayResponse\RelayResponseEvent $message */
foreach ($relayResponses as $message) {
print $message->event->content . PHP_EOL;
if (isset($message->event->content)) {
print $message->event->content . PHP_EOL;
}
}
}
} catch (Exception $e) {
Expand Down
49 changes: 49 additions & 0 deletions src/Message/AuthMessage.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
<?php

declare(strict_types=1);

namespace swentel\nostr\Message;

use swentel\nostr\EventInterface;
use swentel\nostr\MessageInterface;

class AuthMessage implements MessageInterface
{
/**
* @var string $type
*/
private string $type;

/**
* The event.
*
* @var EventInterface
*/
protected EventInterface $event;

public function __construct(EventInterface $event)
{
$this->event = $event;
$this->setType(MessageTypeEnum::AUTH);
}

/**
* Set message type.
*
* @param MessageTypeEnum $type
* @return void
*/
public function setType(MessageTypeEnum $type): void
{
$this->type = $type->value;
}

/**
* {@inheritdoc}
*/
public function generate(): string
{
$event = json_encode($this->event->toArray(), JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE);
return '["' . $this->type . '", ' . $event . ']';
}
}
1 change: 1 addition & 0 deletions src/Message/MessageTypeEnum.php
Original file line number Diff line number Diff line change
Expand Up @@ -12,4 +12,5 @@ enum MessageTypeEnum: string
case EVENT = 'EVENT';
case REQUEST = 'REQ';
case CLOSE = 'CLOSE';
case AUTH = 'AUTH';
}
33 changes: 33 additions & 0 deletions src/Nip42/AuthEvent.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
<?php

declare(strict_types=1);

namespace swentel\nostr\Nip42;

use swentel\nostr\Event\Event;

/**
* NIP-42: https://github.com/nostr-protocol/nips/blob/master/42.md
* AuthEvent class for canonical authentication event.
*/
class AuthEvent extends Event
{
/**
* Event kind for canonical authentication event sent to the relay.
*
* @var int
*/
protected int $kind = 22242;

/**
* Base constructor for AuthEvent.
*/
public function __construct($relayUri, $challenge)
{
parent::__construct();
$this->setTags([
['relay', $relayUri],
['challenge', $challenge],
]);
}
}
89 changes: 81 additions & 8 deletions src/Request/Request.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,18 @@

namespace swentel\nostr\Request;

use swentel\nostr\Event\Event;
use swentel\nostr\Message\AuthMessage;
use swentel\nostr\Nip42\AuthEvent;
use swentel\nostr\Relay\Relay;
use swentel\nostr\Relay\RelaySet;
use swentel\nostr\RelayResponse\RelayResponse;
use swentel\nostr\RequestInterface;
use swentel\nostr\Sign\Sign;
use WebSocket;
use WebSocket\Client;
use WebSocket\Connection;
use WebSocket\Message\Text;

class Request implements RequestInterface
{
Expand All @@ -26,6 +33,13 @@ class Request implements RequestInterface
*/
private string $payload;

/**
* Array with all responses received from the relay.
*
* @var array
*/
protected array $responses;

/**
* Constructor for the Request class.
* Initializes the url and payload properties based on the provided websocket and message.
Expand Down Expand Up @@ -71,7 +85,8 @@ public function send(): array
* Method to send a request using WebSocket client, receive responses, and handle errors.
*
* @param Relay $relay
* @return array
* @return array|RelayResponse
* @throws \Throwable
*/
private function getResponseFromRelay(Relay $relay): array | RelayResponse
{
Expand All @@ -86,9 +101,9 @@ private function getResponseFromRelay(Relay $relay): array | RelayResponse
* connection is still alive, but it does not confirm the closure of the subscription)
*/

$client = new WebSocket\Client($relay->getUrl());
$client = new Client($relay->getUrl());
$client->setTimeout(60);
$client->text($this->payload);
$result = [];

while ($response = $client->receive()) {
if ($response === null) {
Expand All @@ -100,17 +115,75 @@ private function getResponseFromRelay(Relay $relay): array | RelayResponse
return RelayResponse::create($response);
} elseif ($response instanceof WebSocket\Message\Ping) {
$client->disconnect();
return $result;
} elseif ($response instanceof WebSocket\Message\Text) {
return $this->responses;
} elseif ($response instanceof Text) {
$relayResponse = RelayResponse::create(json_decode($response->getContent()));
$this->responses[] = $relayResponse;
if ($relayResponse->type === 'EOSE') {
$client->disconnect();
break;
}

$result[] = $relayResponse;
if ($relayResponse->type === 'OK' && $relayResponse->status === false) {
$client->disconnect();
throw new \Exception($relayResponse->message);
}
// NIP-42
if ($relayResponse->type === 'AUTH') {
// Save challenge string in session.
$_SESSION['challenge'] = $relayResponse->message;
}
if ($relayResponse->type === 'CLOSED') {
// NIP-42
// We do need to broadcast a signed event verification here to the relay.
if (str_starts_with($relayResponse->message, 'auth-required:')) {
if (!isset($_SESSION['challenge'])) {
$client->disconnect();
throw new \Exception('No challenge set in $_SESSION');
}
$aa = new AuthEvent($relay->getUrl(), $_SESSION['challenge']);
$authEvent = new Event();
$authEvent->setKind(22242);
$authEvent->setTags([
['relay', $relay->getUrl()],
['challenge', $_SESSION['challenge']],
]);
$sec = '0000000000000000000000000000000000000000000000000000000000000001';
// todo: use client defined secret key here instead of this default one
$signer = new Sign();
$signer->signEvent($aa, $sec);
$authMessage = new AuthMessage($aa);
$initialMessage = $this->payload;
$this->payload = $authMessage->generate();
$client->text($this->payload);
// Set listener.
$client->onText(function (Client $client, Connection $connection, Text $message) {
$this->responses[] = RelayResponse::create(json_decode($message->getContent()));
$client->stop();
})->start();
// Broadcast the initial message to the relay now the AUTH is done.
$this->payload = $initialMessage;
$client->text($this->payload);
$client->onText(function (Client $client, Connection $connection, Text $message) {
/** @var RelayResponse $response */
$response = RelayResponse::create(json_decode($message->getContent()));
$this->responses[] = $response;
if ($response->type === 'EOSE') {
$client->disconnect();
}
})->start();
break;
}
if (str_starts_with($relayResponse->message, 'restricted:')) {
// For when a client has already performed AUTH but the key used to perform
// it is still not allowed by the relay or is exceeding its authorization.
$client->disconnect();
throw new \Exception($relayResponse->message);
}
}
}
}
$client->disconnect();
return $result;
$client->close();
return $this->responses;
}
}
6 changes: 5 additions & 1 deletion tests/RelayResponseTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -7,14 +7,16 @@
use swentel\nostr\Message\RequestMessage;
use swentel\nostr\Relay\Relay;
use swentel\nostr\RelayResponse\RelayResponseAuth;
use swentel\nostr\RelayResponse\RelayResponseClosed;
use swentel\nostr\RelayResponse\RelayResponseOk;
use swentel\nostr\Request\Request;
use swentel\nostr\Subscription\Subscription;

class RelayResponseTest extends TestCase
{
public function testSendRequestToRelayAndResultAuth()
{
$relayUrl = 'wss://nostr.sebastix.social';
$relayUrl = 'wss://jingle.nostrver.se';

$relay = new Relay($relayUrl);

Expand All @@ -33,5 +35,7 @@ public function testSendRequestToRelayAndResultAuth()
$result = $request->send();

$this->assertInstanceOf(RelayResponseAuth::class, $result[$relayUrl][0]);
$this->assertInstanceOf(RelayResponseClosed::class, $result[$relayUrl][1]);
$this->assertInstanceOf(RelayResponseOk::class, $result[$relayUrl][2]);
}
}

0 comments on commit 10bcf1f

Please sign in to comment.