Skip to content

Commit

Permalink
Merge pull request #82 from relaystr/nip-42-new
Browse files Browse the repository at this point in the history
nip-42
  • Loading branch information
frnandu authored Dec 25, 2024
2 parents 5683fda + 43e7560 commit 85f4b0c
Show file tree
Hide file tree
Showing 9 changed files with 178 additions and 26 deletions.
1 change: 1 addition & 0 deletions packages/ndk/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -170,6 +170,7 @@ await for (final event in response.stream) {
- [x] Lists ([NIP-51](https://github.com/nostr-protocol/nips/blob/master/51.md))
- [x] Relay List Metadata ([NIP-65](https://github.com/nostr-protocol/nips/blob/master/65.md))
- [x] Wallet Connect API ([NIP-47](https://github.com/nostr-protocol/nips/blob/master/47.md))
- [x] Authentication of clients to relays ([NIP-42](https://github.com/nostr-protocol/nips/blob/master/42.md))
- [ ] Bech Encoding support ([NIP-19](https://github.com/nostr-protocol/nips/blob/master/19.md))
- [ ] Zaps (private, public, anon, non-zap) ([NIP-57](https://github.com/nostr-protocol/nips/blob/master/57.md))
- [ ] Badges ([NIP-58](https://github.com/nostr-protocol/nips/blob/master/58.md))
Expand Down
19 changes: 7 additions & 12 deletions packages/ndk/lib/domain_layer/entities/nip_01_event.dart
Original file line number Diff line number Diff line change
Expand Up @@ -131,17 +131,7 @@ class Nip01Event {

/// return first `e` tag found
String? getEId() {
for (var tag in tags) {
if (tag.length > 1) {
var key = tag[0];
var value = tag[1];

if (key == "e") {
return value;
}
}
}
return null;
return getFirstTag("e");
}

/// return all tags that match given `tag`
Expand Down Expand Up @@ -189,12 +179,17 @@ class Nip01Event {

/// return first found `d` tag
String? getDtag() {
return getFirstTag("d");
}

/// Get first tag matching given name
String? getFirstTag(String name) {
for (var tag in tags) {
if (tag.length > 1) {
var key = tag[0];
var value = tag[1];

if (key == "d") {
if (key == name) {
return value;
}
}
Expand Down
14 changes: 14 additions & 0 deletions packages/ndk/lib/domain_layer/usecases/nip42/auth_event.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import 'package:ndk/domain_layer/entities/nip_01_event.dart';

/// auth event to send to relays
class AuthEvent extends Nip01Event {
/// auth kind
// ignore: constant_identifier_names
static const int KIND = 22242;

/// Zap Request
AuthEvent({
required super.pubKey,
required super.tags,
}) : super(kind: KIND, content: '');
}
42 changes: 30 additions & 12 deletions packages/ndk/lib/domain_layer/usecases/relay_manager.dart
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import 'dart:async';
import 'dart:convert';
import 'dart:developer' as developer;

import '../../config/bootstrap_relays.dart';
import '../../config/relay_defaults.dart';
Expand All @@ -16,8 +17,10 @@ import '../entities/relay_connectivity.dart';
import '../entities/relay_info.dart';
import '../entities/request_state.dart';
import '../entities/tuple.dart';
import '../repositories/event_signer.dart';
import '../repositories/nostr_transport.dart';
import 'engines/network_engine.dart';
import 'nip42/auth_event.dart';

/// relay manager, responsible for lifecycle of relays, sending messages, \
/// and help with tracking of requests
Expand All @@ -30,6 +33,9 @@ class RelayManager<T> {
/// global state obj
GlobalState globalState;

/// signer for nip-42 AUTH challenges from relays
EventSigner? signer;

/// nostr transport factory, to create new transports (usually websocket)
final NostrTransportFactory nostrTransportFactory;

Expand All @@ -43,6 +49,7 @@ class RelayManager<T> {
RelayManager(
{required this.globalState,
required this.nostrTransportFactory,
this.signer,
this.engineAdditionalDataFactory,
List<String>? bootstrapRelays,
allowReconnect = true}) {
Expand All @@ -54,7 +61,7 @@ class RelayManager<T> {
bool get allowReconnectRelays => _allowReconnectRelays;

/// sets allowed to reconnectRelays
set allowReconnectRelays(bool b) {
void set allowReconnectRelays(bool b) {
_allowReconnectRelays = b;
}

Expand Down Expand Up @@ -101,7 +108,7 @@ class RelayManager<T> {

/// checks if a relay is connecting
bool isRelayConnecting(String url) {
Relay? relay = globalState.relays[url]?.relay;
Relay? relay = globalState.relays[url]?.relay ?? null;
return relay != null && relay.connecting;
}

Expand Down Expand Up @@ -138,6 +145,7 @@ class RelayManager<T> {
);
globalState.relays[url] = relayConnectivity;
}
;
relayConnectivity.relay.tryingToConnect();

/// TO BE REMOVED, ONCE WE FIND A WAY OF AVOIDING PROBLEM WHEN CONNECTING TO THIS
Expand All @@ -159,7 +167,7 @@ class RelayManager<T> {

_startListeningToSocket(relayConnectivity);

Logger.log.t("connected to relay: $url");
developer.log("connected to relay: $url");
relayConnectivity.relay.succeededToConnect();
relayConnectivity.stats.connections++;
getRelayInfo(url).then((info) {
Expand All @@ -183,7 +191,7 @@ class RelayManager<T> {
await relayConnectivity.relayTransport!.ready
.timeout(Duration(seconds: DEFAULT_WEB_SOCKET_CONNECT_TIMEOUT))
.onError((error, stackTrace) {
Logger.log.e("error connecting to relay $url: $error");
Logger.log.e("error connecting to relay ${url}: $error");
return []; // Return an empty list in case of error
});
}
Expand Down Expand Up @@ -301,7 +309,7 @@ class RelayManager<T> {
} else {
// do not overwrite
Logger.log.w(
"registerRelayBroadcast: relay broadcast already registered for ${eventToPublish.id} $relayUrl, skipping");
"registerRelayBroadcast: relay broadcast already registered for ${eventToPublish.id} ${relayUrl}, skipping");
}
}

Expand Down Expand Up @@ -373,12 +381,22 @@ class RelayManager<T> {
" CLOSED subscription url: ${relayConnectivity.url} id: ${eventJson[1]} msg: ${eventJson.length > 2 ? eventJson[2] : ''}");
globalState.inFlightRequests.remove(eventJson[1]);
}
// TODO
// if (eventJson[0] == 'AUTH') {
// log("AUTH: ${eventJson[1]}");
// // nip 42 used to send authentication challenges
// return;
// }
if (eventJson[0] == ClientMsgType.AUTH) {
// nip 42 used to send authentication challenges
final challenge = eventJson[1];
Logger.log.d("AUTH: $challenge");
if (signer != null && signer!.canSign()) {
final auth = AuthEvent(pubKey: signer!.getPublicKey(), tags: [
["relay", relayConnectivity.url],
["challenge", challenge]
]);
signer!.sign(auth);
send(relayConnectivity, ClientMsg(ClientMsgType.AUTH, event: auth));
} else {
Logger.log.w("Received an AUTH challenge but don't have a signer configured");
}
return;
}
//
// if (eventJson[0] == 'COUNT') {
// log("COUNT: ${eventJson[1]}");
Expand All @@ -402,7 +420,7 @@ class RelayManager<T> {
if (state != null) {
RelayRequestState? request = state.requests[url];
if (request == null) {
Logger.log.w("No RelayRequestState found for id $id");
Logger.log.w("No RelayRequestState found for id ${id}");
return;
}
event.sources.add(url);
Expand Down
1 change: 1 addition & 0 deletions packages/ndk/lib/presentation_layer/init.dart
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,7 @@ class Initialization {
case NdkEngine.RELAY_SETS:
relayManager = RelayManager(
globalState: _globalState,
signer: _ndkConfig.eventSigner,
nostrTransportFactory: _webSocketNostrTransportFactory,
bootstrapRelays: _ndkConfig.bootstrapRelays,
);
Expand Down
8 changes: 8 additions & 0 deletions packages/ndk/lib/shared/nips/nip01/client_msg.dart
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,11 @@ class ClientMsg {
if (type == ClientMsgType.COUNT) {
throw Exception("COUNT is not implemented yet");
}
if (type == ClientMsgType.AUTH) {
if (event == null) {
throw Exception("event is required for type AUTH");
}
}
}

_eventToJson() {
Expand All @@ -74,6 +79,8 @@ class ClientMsg {
return _reqToJson();
case ClientMsgType.CLOSE:
return _closeToJson();
case ClientMsgType.AUTH:
return _eventToJson();
case ClientMsgType.COUNT:
throw Exception("COUNT is not implemented yet");
}
Expand All @@ -90,4 +97,5 @@ class ClientMsgType {
static const String CLOSE = "CLOSE";
static const String EVENT = "EVENT";
static const String COUNT = "COUNT";
static const String AUTH = "AUTH";
}
29 changes: 29 additions & 0 deletions packages/ndk/test/mocks/mock_relay.dart
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,11 @@ import 'dart:convert';
import 'dart:developer';
import 'dart:io';

import 'package:bip340/bip340.dart';
import 'package:ndk/data_layer/repositories/verifiers/bip340_event_verifier.dart';
import 'package:ndk/domain_layer/repositories/event_verifier.dart';
import 'package:ndk/entities.dart';
import 'package:ndk/shared/nips/nip01/helpers.dart';
import 'package:ndk/shared/nips/nip01/key_pair.dart';

class MockRelay {
Expand All @@ -14,6 +18,7 @@ class MockRelay {
Map<KeyPair, Nip65>? nip65s;
Map<KeyPair, Nip01Event>? textNotes;
bool signEvents;
bool requireAuthForRequests;

static int startPort = 4040;

Expand All @@ -23,6 +28,7 @@ class MockRelay {
required this.name,
this.nip65s,
this.signEvents = true,
this.requireAuthForRequests = false,
int? explicitPort,
}) {
if (explicitPort != null) {
Expand Down Expand Up @@ -52,14 +58,37 @@ class MockRelay {

var stream = server.transform(WebSocketTransformer());

String challenge='';

bool signedChallenge=false;
stream.listen((webSocket) {
this.webSocket = webSocket;
if (requireAuthForRequests && !signedChallenge) {
challenge = Helpers.getRandomString(10);
webSocket.add(jsonEncode(["AUTH", challenge]));
}
webSocket.listen((message) {
if (message == "ping") {
webSocket.add("pong");
return;
}
var eventJson = json.decode(message);
if (eventJson[0] == "AUTH") {
Nip01Event event = Nip01Event.fromJson(eventJson[1]);
if (verify(event.pubKey, event.id, event.sig)) {
String? relay = event.getFirstTag("relay");
String? eventChallenge = event.getFirstTag("challenge");
if (eventChallenge==challenge && relay==url) {
signedChallenge = true;
}
}
webSocket.add(jsonEncode(["OK", event.id, signedChallenge, signedChallenge?"":"auth-required: we can't serve requests to unauthenticated users"]));
return;
}
if (requireAuthForRequests && !signedChallenge) {
webSocket.add(jsonEncode(["CLOSED", "sub_1","auth-required: we can't serve requests to unauthenticated users"]));
return;
}
if (eventJson[0] == "REQ") {
String requestId = eventJson[1];
log('Received: $eventJson');
Expand Down
87 changes: 87 additions & 0 deletions packages/ndk/test/relays/nip42_test.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
// ignore_for_file: avoid_print

import 'dart:async';

import 'package:ndk/data_layer/repositories/nostr_transport/websocket_client_nostr_transport_factory.dart';
import 'package:ndk/data_layer/repositories/signers/bip340_event_signer.dart';
import 'package:ndk/domain_layer/entities/global_state.dart';
import 'package:ndk/domain_layer/usecases/relay_manager.dart';
import 'package:ndk/entities.dart';
import 'package:ndk/ndk.dart';
import 'package:ndk/shared/nips/nip01/bip340.dart';
import 'package:ndk/shared/nips/nip01/key_pair.dart';
import 'package:test/test.dart';

import '../mocks/mock_relay.dart';

void main() async {
group('NIP-42', () {

KeyPair key1 = Bip340.generatePrivateKey();

Map<KeyPair, String> keyNames = {
key1: "key1",
};

Nip01Event textNote(KeyPair key2) {
return Nip01Event(
kind: Nip01Event.TEXT_NODE_KIND,
pubKey: key2.publicKey,
content: "some note from key ${keyNames[key1]}",
tags: [],
createdAt: DateTime.now().millisecondsSinceEpoch ~/ 1000);
}

Map<KeyPair, Nip01Event> key1TextNotes = {key1: textNote(key1)};

test('respond to auth challenge', () async {
MockRelay relay1 = MockRelay(name: "relay 1", explicitPort: 3900, requireAuthForRequests: true);
await relay1.startServer(textNotes: key1TextNotes);

final ndk = Ndk(
NdkConfig(
eventVerifier: Bip340EventVerifier(),
eventSigner: Bip340EventSigner(
privateKey: key1.privateKey,
publicKey: key1.publicKey,
),
cache: MemCacheManager(),
logLevel: Logger.logLevels.trace,
bootstrapRelays: [relay1.url]),
);

await Future.delayed(Duration(seconds: 1));
final response = ndk.requests.query(filters: [
Filter(kinds: [Nip01Event.TEXT_NODE_KIND], authors: [key1.publicKey])
]);
await expectLater(response.stream, emitsInAnyOrder(key1TextNotes.values));

// TODO write some events
// TODO do some requests
await ndk.destroy();
await relay1.stopServer();
});

test("check that relay does not return events if we don't provide a signer", () async {
MockRelay relay1 = MockRelay(name: "relay 1", explicitPort: 3900, requireAuthForRequests: true);
await relay1.startServer(textNotes: key1TextNotes);

final ndk = Ndk(
NdkConfig(
eventVerifier: Bip340EventVerifier(),
cache: MemCacheManager(),
logLevel: Logger.logLevels.trace,
bootstrapRelays: [relay1.url]),
);

await Future.delayed(Duration(seconds: 1));
final response = ndk.requests.query(filters: [
Filter(kinds: [Nip01Event.TEXT_NODE_KIND], authors: [key1.publicKey])
]);
List<Nip01Event> events = await response.future;
expect(events, isEmpty);
await ndk.destroy();
await relay1.stopServer();
});
});
}
3 changes: 1 addition & 2 deletions packages/ndk/test/relays/relay_manager_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,9 @@
import 'dart:async';

import 'package:ndk/data_layer/repositories/nostr_transport/websocket_client_nostr_transport_factory.dart';
import 'package:ndk/domain_layer/entities/global_state.dart';
import 'package:ndk/domain_layer/usecases/relay_manager.dart';
import 'package:ndk/entities.dart';
import 'package:test/test.dart';
import 'package:ndk/data_layer/repositories/nostr_transport/websocket_nostr_transport_factory.dart';

import '../mocks/mock_relay.dart';

Expand Down Expand Up @@ -38,6 +36,7 @@ void main() async {
test('Try to connect to dead relay', () async {
RelayManager manager = RelayManager(
nostrTransportFactory: webSocketNostrTransportFactory,
bootstrapRelays: [],
globalState: GlobalState(),
);

Expand Down

0 comments on commit 85f4b0c

Please sign in to comment.