From 7c4e32cab3f2bdbeadd7d88d41f3bc046c627290 Mon Sep 17 00:00:00 2001 From: Filip Borkiewicz Date: Sat, 15 Oct 2022 17:36:58 +0200 Subject: [PATCH] Make connecting via rooms possible (#77) --- di/inject_adapters.go | 2 + di/inject_application.go | 4 + di/inject_networking.go | 31 ++ di/inject_ports.go | 2 + di/inject_pubsub.go | 7 + di/service.go | 36 ++- di/wire.go | 56 ++-- di/wire_gen.go | 70 +++-- fixtures/fixtures.go | 6 +- service/adapters/mocks/dialer.go | 2 +- service/adapters/mocks/peer_manager.go | 17 -- .../adapters/pubsub/attendant_event_pubsub.go | 35 +++ service/app/application.go | 5 +- service/app/commands/common.go | 11 + .../handler_process_room_attendant_event.go | 62 ++++ ...ndler_process_room_attendant_event_test.go | 96 ++++++ service/app/commands/handler_redeem_invite.go | 11 +- .../commands/handler_rooms_alias_register.go | 32 +- .../handler_rooms_alias_register_test.go | 2 +- .../commands/handler_rooms_alias_revoke.go | 10 +- .../handler_rooms_alias_revoke_test.go | 2 +- .../app/queries/handler_rooms_list_aliases.go | 6 +- .../handler_rooms_list_aliases_test.go | 14 +- service/app/queries/status_test.go | 9 +- .../blobs/replication/has_handler_test.go | 5 +- .../domain/blobs/replication/manager_test.go | 3 +- .../domain/blobs/replication/wants_test.go | 7 +- service/domain/messages/blobs_createWants.go | 6 +- service/domain/messages/blobs_get.go | 6 +- .../domain/messages/create_history_stream.go | 6 +- service/domain/messages/ebt_replicate.go | 6 +- service/domain/messages/invite_use.go | 6 +- service/domain/messages/room_attendants.go | 124 ++++++++ .../domain/messages/room_attendants_test.go | 82 +++++ service/domain/messages/room_listAliases.go | 16 +- .../domain/messages/room_listAliases_test.go | 38 +++ service/domain/messages/room_metadata.go | 84 +++++ service/domain/messages/room_metadata_test.go | 58 ++++ service/domain/messages/room_registerAlias.go | 16 +- .../messages/room_registerAlias_test.go | 55 ++++ service/domain/messages/room_revokeAlias.go | 6 +- .../domain/messages/room_revokeAlias_test.go | 24 ++ service/domain/messages/tunnel_connect.go | 66 ++++ .../domain/messages/tunnel_connect_test.go | 28 ++ .../{transport => }/mocks/connection.go | 0 service/domain/mocks/peer_manager.go | 57 ++++ service/domain/network/dialer.go | 11 +- service/domain/peer_manager.go | 95 ++++-- service/domain/peer_manager_test.go | 50 ++- .../domain/replication/ebt/replicator_test.go | 8 +- service/domain/replication/negotiator.go | 2 +- service/domain/replication/negotiator_test.go | 4 +- .../rooms/{rooms.go => aliases/aliases.go} | 10 +- .../aliases_test.go} | 20 +- service/domain/rooms/features/features.go | 56 ++++ .../domain/rooms/features/features_test.go | 52 ++++ service/domain/rooms/rpc.go | 223 ++++++++++++++ service/domain/rooms/rpc_test.go | 101 ++++++ service/domain/rooms/scanner.go | 87 ++++++ service/domain/rooms/scanner_test.go | 154 ++++++++++ service/domain/rooms/tunnel/tunnel.go | 108 +++++++ service/domain/rooms/tunnel/tunnel_test.go | 288 ++++++++++++++++++ .../domain/transport/boxstream/handshake.go | 41 ++- .../transport/boxstream/handshake_test.go | 83 ++++- .../domain/transport/boxstream/stream_test.go | 4 +- service/domain/transport/peer.go | 23 +- service/domain/transport/peer_initializer.go | 7 +- service/domain/transport/rpc/connection.go | 9 +- .../domain/transport/rpc/response_streams.go | 1 + .../transport/rpc/transport/raw_connection.go | 9 +- service/ports/pubsub/requests.go | 4 +- service/ports/pubsub/room_attendant_events.go | 49 +++ .../pubsub/room_attendant_events_test.go | 71 +++++ 73 files changed, 2540 insertions(+), 257 deletions(-) create mode 100644 di/inject_networking.go delete mode 100644 service/adapters/mocks/peer_manager.go create mode 100644 service/adapters/pubsub/attendant_event_pubsub.go create mode 100644 service/app/commands/handler_process_room_attendant_event.go create mode 100644 service/app/commands/handler_process_room_attendant_event_test.go create mode 100644 service/domain/messages/room_attendants.go create mode 100644 service/domain/messages/room_attendants_test.go create mode 100644 service/domain/messages/room_listAliases_test.go create mode 100644 service/domain/messages/room_metadata.go create mode 100644 service/domain/messages/room_metadata_test.go create mode 100644 service/domain/messages/room_registerAlias_test.go create mode 100644 service/domain/messages/room_revokeAlias_test.go create mode 100644 service/domain/messages/tunnel_connect.go create mode 100644 service/domain/messages/tunnel_connect_test.go rename service/domain/{transport => }/mocks/connection.go (100%) create mode 100644 service/domain/mocks/peer_manager.go rename service/domain/rooms/{rooms.go => aliases/aliases.go} (95%) rename service/domain/rooms/{rooms_test.go => aliases/aliases_test.go} (82%) create mode 100644 service/domain/rooms/features/features.go create mode 100644 service/domain/rooms/features/features_test.go create mode 100644 service/domain/rooms/rpc.go create mode 100644 service/domain/rooms/rpc_test.go create mode 100644 service/domain/rooms/scanner.go create mode 100644 service/domain/rooms/scanner_test.go create mode 100644 service/domain/rooms/tunnel/tunnel.go create mode 100644 service/domain/rooms/tunnel/tunnel_test.go create mode 100644 service/ports/pubsub/room_attendant_events.go create mode 100644 service/ports/pubsub/room_attendant_events_test.go diff --git a/di/inject_adapters.go b/di/inject_adapters.go index 5160fbb4..46208070 100644 --- a/di/inject_adapters.go +++ b/di/inject_adapters.go @@ -17,6 +17,7 @@ import ( "github.com/planetary-social/scuttlego/service/domain/identity" "github.com/planetary-social/scuttlego/service/domain/replication" "github.com/planetary-social/scuttlego/service/domain/replication/ebt" + "github.com/planetary-social/scuttlego/service/domain/transport/boxstream" "go.etcd.io/bbolt" ) @@ -98,6 +99,7 @@ func newFilesystemStorage(logger logging.Logger, config Config) (*blobs.Filesyst var adaptersSet = wire.NewSet( adapters.NewCurrentTimeProvider, wire.Bind(new(commands.CurrentTimeProvider), new(*adapters.CurrentTimeProvider)), + wire.Bind(new(boxstream.CurrentTimeProvider), new(*adapters.CurrentTimeProvider)), adapters.NewBanListHasher, wire.Bind(new(bolt.BanListHasher), new(*adapters.BanListHasher)), diff --git a/di/inject_application.go b/di/inject_application.go index ccc20e67..7c82728c 100644 --- a/di/inject_application.go +++ b/di/inject_application.go @@ -7,6 +7,7 @@ import ( "github.com/planetary-social/scuttlego/service/app/commands" "github.com/planetary-social/scuttlego/service/app/queries" "github.com/planetary-social/scuttlego/service/domain/replication" + "github.com/planetary-social/scuttlego/service/ports/pubsub" portsrpc "github.com/planetary-social/scuttlego/service/ports/rpc" ) @@ -45,6 +46,9 @@ var commandsSet = wire.NewSet( commands.NewRoomsAliasRegisterHandler, commands.NewRoomsAliasRevokeHandler, + + commands.NewProcessRoomAttendantEventHandler, + wire.Bind(new(pubsub.ProcessRoomAttendantEventHandler), new(*commands.ProcessRoomAttendantEventHandler)), ) var queriesSet = wire.NewSet( diff --git a/di/inject_networking.go b/di/inject_networking.go new file mode 100644 index 00000000..e72e48ce --- /dev/null +++ b/di/inject_networking.go @@ -0,0 +1,31 @@ +package di + +import ( + "github.com/google/wire" + "github.com/planetary-social/scuttlego/service/app/commands" + "github.com/planetary-social/scuttlego/service/app/queries" + "github.com/planetary-social/scuttlego/service/domain" + "github.com/planetary-social/scuttlego/service/domain/network" + "github.com/planetary-social/scuttlego/service/domain/rooms/tunnel" + domaintransport "github.com/planetary-social/scuttlego/service/domain/transport" + "github.com/planetary-social/scuttlego/service/domain/transport/boxstream" + "github.com/planetary-social/scuttlego/service/domain/transport/rpc" + portsnetwork "github.com/planetary-social/scuttlego/service/ports/network" +) + +//nolint:unused +var networkingSet = wire.NewSet( + domaintransport.NewPeerInitializer, + wire.Bind(new(portsnetwork.ServerPeerInitializer), new(*domaintransport.PeerInitializer)), + wire.Bind(new(network.ClientPeerInitializer), new(*domaintransport.PeerInitializer)), + wire.Bind(new(tunnel.ClientPeerInitializer), new(*domaintransport.PeerInitializer)), + + rpc.NewConnectionIdGenerator, + + boxstream.NewHandshaker, + + network.NewDialer, + wire.Bind(new(commands.Dialer), new(*network.Dialer)), + wire.Bind(new(queries.Dialer), new(*network.Dialer)), + wire.Bind(new(domain.Dialer), new(*network.Dialer)), +) diff --git a/di/inject_ports.go b/di/inject_ports.go index 24b80bde..556b1660 100644 --- a/di/inject_ports.go +++ b/di/inject_ports.go @@ -27,6 +27,8 @@ var portsSet = wire.NewSet( portspubsub.NewRequestSubscriber, + portspubsub.NewRoomAttendantEventSubscriber, + local.NewDiscoverer, portsnetwork.NewDiscoverer, portsnetwork.NewConnectionEstablisher, diff --git a/di/inject_pubsub.go b/di/inject_pubsub.go index a119e9db..9de14b71 100644 --- a/di/inject_pubsub.go +++ b/di/inject_pubsub.go @@ -5,6 +5,7 @@ import ( "github.com/planetary-social/scuttlego/service/adapters/pubsub" "github.com/planetary-social/scuttlego/service/app/queries" blobReplication "github.com/planetary-social/scuttlego/service/domain/blobs/replication" + "github.com/planetary-social/scuttlego/service/domain/rooms" "github.com/planetary-social/scuttlego/service/domain/transport/rpc" ) @@ -13,6 +14,7 @@ var pubSubSet = wire.NewSet( requestPubSubSet, messagePubSubSet, blobDownloadedPubSubSet, + roomAttendantEventPubSubSet, ) var requestPubSubSet = wire.NewSet( @@ -30,3 +32,8 @@ var blobDownloadedPubSubSet = wire.NewSet( wire.Bind(new(queries.BlobDownloadedSubscriber), new(*pubsub.BlobDownloadedPubSub)), wire.Bind(new(blobReplication.BlobDownloadedPublisher), new(*pubsub.BlobDownloadedPubSub)), ) + +var roomAttendantEventPubSubSet = wire.NewSet( + pubsub.NewRoomAttendantEventPubSub, + wire.Bind(new(rooms.AttendantEventPublisher), new(*pubsub.RoomAttendantEventPubSub)), +) diff --git a/di/service.go b/di/service.go index ce4f9c39..ee3a44e9 100644 --- a/di/service.go +++ b/di/service.go @@ -16,13 +16,14 @@ import ( type Service struct { App app.Application - listener *networkport.Listener - discoverer *networkport.Discoverer - connectionEstablisher *networkport.ConnectionEstablisher - requestSubscriber *pubsubport.RequestSubscriber - advertiser *local.Advertiser - messageBuffer *commands.MessageBuffer - createHistoryStreamHandler *queries.CreateHistoryStreamHandler + listener *networkport.Listener + discoverer *networkport.Discoverer + connectionEstablisher *networkport.ConnectionEstablisher + requestSubscriber *pubsubport.RequestSubscriber + roomAttendantEventSubscriber *pubsubport.RoomAttendantEventSubscriber + advertiser *local.Advertiser + messageBuffer *commands.MessageBuffer + createHistoryStreamHandler *queries.CreateHistoryStreamHandler } func NewService( @@ -31,6 +32,7 @@ func NewService( discoverer *networkport.Discoverer, connectionEstablisher *networkport.ConnectionEstablisher, requestSubscriber *pubsubport.RequestSubscriber, + roomAttendantEventSubscriber *pubsubport.RoomAttendantEventSubscriber, advertiser *local.Advertiser, messageBuffer *commands.MessageBuffer, createHistoryStreamHandler *queries.CreateHistoryStreamHandler, @@ -38,13 +40,14 @@ func NewService( return Service{ App: app, - listener: listener, - discoverer: discoverer, - connectionEstablisher: connectionEstablisher, - requestSubscriber: requestSubscriber, - advertiser: advertiser, - messageBuffer: messageBuffer, - createHistoryStreamHandler: createHistoryStreamHandler, + listener: listener, + discoverer: discoverer, + connectionEstablisher: connectionEstablisher, + requestSubscriber: requestSubscriber, + roomAttendantEventSubscriber: roomAttendantEventSubscriber, + advertiser: advertiser, + messageBuffer: messageBuffer, + createHistoryStreamHandler: createHistoryStreamHandler, } } @@ -65,6 +68,11 @@ func (s Service) Run(ctx context.Context) error { errCh <- s.requestSubscriber.Run(ctx) }() + runners++ + go func() { + errCh <- s.roomAttendantEventSubscriber.Run(ctx) + }() + runners++ go func() { errCh <- s.advertiser.Run(ctx) diff --git a/di/wire.go b/di/wire.go index 501805fd..f7c72d26 100644 --- a/di/wire.go +++ b/di/wire.go @@ -24,15 +24,13 @@ import ( "github.com/planetary-social/scuttlego/service/domain/feeds/formats" "github.com/planetary-social/scuttlego/service/domain/graph" "github.com/planetary-social/scuttlego/service/domain/identity" - "github.com/planetary-social/scuttlego/service/domain/network" + domainmocks "github.com/planetary-social/scuttlego/service/domain/mocks" "github.com/planetary-social/scuttlego/service/domain/network/local" "github.com/planetary-social/scuttlego/service/domain/replication" "github.com/planetary-social/scuttlego/service/domain/replication/ebt" "github.com/planetary-social/scuttlego/service/domain/replication/gossip" - domaintransport "github.com/planetary-social/scuttlego/service/domain/transport" - "github.com/planetary-social/scuttlego/service/domain/transport/boxstream" - "github.com/planetary-social/scuttlego/service/domain/transport/rpc" - portsnetwork "github.com/planetary-social/scuttlego/service/ports/network" + "github.com/planetary-social/scuttlego/service/domain/rooms" + "github.com/planetary-social/scuttlego/service/domain/rooms/tunnel" "go.etcd.io/bbolt" ) @@ -95,10 +93,12 @@ func BuildTestAdapters(*bbolt.DB) (TestAdapters, error) { } type TestCommands struct { - RoomsAliasRegister *commands.RoomsAliasRegisterHandler - RoomsAliasRevoke *commands.RoomsAliasRevokeHandler + RoomsAliasRegister *commands.RoomsAliasRegisterHandler + RoomsAliasRevoke *commands.RoomsAliasRevokeHandler + ProcessRoomAttendantEvent *commands.ProcessRoomAttendantEventHandler - Dialer *mocks.DialerMock + PeerManager *domainmocks.PeerManagerMock + Dialer *mocks.DialerMock } func BuildTestCommands(*testing.T) (TestCommands, error) { @@ -108,6 +108,9 @@ func BuildTestCommands(*testing.T) (TestCommands, error) { mocks.NewDialerMock, wire.Bind(new(commands.Dialer), new(*mocks.DialerMock)), + domainmocks.NewPeerManagerMock, + wire.Bind(new(commands.PeerManager), new(*domainmocks.PeerManagerMock)), + identity.NewPrivate, wire.Struct(new(TestCommands), "*"), @@ -122,7 +125,7 @@ type TestQueries struct { FeedRepository *mocks.FeedRepositoryMock MessagePubSub *mocks.MessagePubSubMock MessageRepository *mocks.MessageRepositoryMock - PeerManager *mocks.PeerManagerMock + PeerManager *domainmocks.PeerManagerMock BlobStorage *mocks.BlobStorageMock ReceiveLogRepository *mocks.ReceiveLogRepositoryMock Dialer *mocks.DialerMock @@ -139,8 +142,8 @@ func BuildTestQueries(*testing.T) (TestQueries, error) { mocks.NewMessagePubSubMock, wire.Bind(new(queries.MessageSubscriber), new(*mocks.MessagePubSubMock)), - mocks.NewPeerManagerMock, - wire.Bind(new(queries.PeerManager), new(*mocks.PeerManagerMock)), + domainmocks.NewPeerManagerMock, + wire.Bind(new(queries.PeerManager), new(*domainmocks.PeerManagerMock)), identity.NewPrivate, privateIdentityToPublicIdentity, @@ -197,21 +200,6 @@ func BuildService(context.Context, identity.Private, Config) (Service, error) { wire.Build( NewService, - extractFromConfigSet, - - boxstream.NewHandshaker, - - domaintransport.NewPeerInitializer, - wire.Bind(new(portsnetwork.ServerPeerInitializer), new(*domaintransport.PeerInitializer)), - wire.Bind(new(network.ClientPeerInitializer), new(*domaintransport.PeerInitializer)), - - rpc.NewConnectionIdGenerator, - - network.NewDialer, - wire.Bind(new(commands.Dialer), new(*network.Dialer)), - wire.Bind(new(queries.Dialer), new(*network.Dialer)), - wire.Bind(new(domain.Dialer), new(*network.Dialer)), - domain.NewPeerManager, wire.Bind(new(commands.NewPeerHandler), new(*domain.PeerManager)), wire.Bind(new(commands.PeerManager), new(*domain.PeerManager)), @@ -221,11 +209,23 @@ func BuildService(context.Context, identity.Private, Config) (Service, error) { wire.Bind(new(commands.TransactionProvider), new(*bolt.TransactionProvider)), newAdaptersFactory, + newBolt, + newAdvertiser, privateIdentityToPublicIdentity, commands.NewMessageBuffer, + rooms.NewScanner, + wire.Bind(new(domain.RoomScanner), new(*rooms.Scanner)), + + rooms.NewPeerRPCAdapter, + wire.Bind(new(rooms.MetadataGetter), new(*rooms.PeerRPCAdapter)), + wire.Bind(new(rooms.AttendantsGetter), new(*rooms.PeerRPCAdapter)), + + tunnel.NewDialer, + wire.Bind(new(domain.RoomDialer), new(*tunnel.Dialer)), + portsSet, applicationSet, replicatorSet, @@ -235,8 +235,8 @@ func BuildService(context.Context, identity.Private, Config) (Service, error) { boltAdaptersSet, blobsAdaptersSet, adaptersSet, - - newBolt, + extractFromConfigSet, + networkingSet, ) return Service{}, nil } diff --git a/di/wire_gen.go b/di/wire_gen.go index d7fcb909..d8f92e4b 100644 --- a/di/wire_gen.go +++ b/di/wire_gen.go @@ -30,11 +30,14 @@ import ( "github.com/planetary-social/scuttlego/service/domain/feeds/formats" "github.com/planetary-social/scuttlego/service/domain/graph" "github.com/planetary-social/scuttlego/service/domain/identity" + mocks2 "github.com/planetary-social/scuttlego/service/domain/mocks" "github.com/planetary-social/scuttlego/service/domain/network" "github.com/planetary-social/scuttlego/service/domain/network/local" "github.com/planetary-social/scuttlego/service/domain/replication" "github.com/planetary-social/scuttlego/service/domain/replication/ebt" "github.com/planetary-social/scuttlego/service/domain/replication/gossip" + "github.com/planetary-social/scuttlego/service/domain/rooms" + "github.com/planetary-social/scuttlego/service/domain/rooms/tunnel" transport2 "github.com/planetary-social/scuttlego/service/domain/transport" "github.com/planetary-social/scuttlego/service/domain/transport/boxstream" "github.com/planetary-social/scuttlego/service/domain/transport/rpc" @@ -121,10 +124,14 @@ func BuildTestCommands(t *testing.T) (TestCommands, error) { } roomsAliasRegisterHandler := commands.NewRoomsAliasRegisterHandler(dialerMock, private) roomsAliasRevokeHandler := commands.NewRoomsAliasRevokeHandler(dialerMock) + peerManagerMock := mocks2.NewPeerManagerMock() + processRoomAttendantEventHandler := commands.NewProcessRoomAttendantEventHandler(peerManagerMock) testCommands := TestCommands{ - RoomsAliasRegister: roomsAliasRegisterHandler, - RoomsAliasRevoke: roomsAliasRevokeHandler, - Dialer: dialerMock, + RoomsAliasRegister: roomsAliasRegisterHandler, + RoomsAliasRevoke: roomsAliasRevokeHandler, + ProcessRoomAttendantEvent: processRoomAttendantEventHandler, + PeerManager: peerManagerMock, + Dialer: dialerMock, } return testCommands, nil } @@ -147,7 +154,7 @@ func BuildTestQueries(t *testing.T) (TestQueries, error) { return TestQueries{}, err } messageRepositoryMock := mocks.NewMessageRepositoryMock() - peerManagerMock := mocks.NewPeerManagerMock() + peerManagerMock := mocks2.NewPeerManagerMock() statusHandler := queries.NewStatusHandler(messageRepositoryMock, feedRepositoryMock, peerManagerMock) blobStorageMock := mocks.NewBlobStorageMock() getBlobHandler, err := queries.NewGetBlobHandler(blobStorageMock) @@ -258,7 +265,8 @@ var ( // e.g. established connections. func BuildService(contextContext context.Context, private identity.Private, config Config) (Service, error) { networkKey := extractNetworkKeyFromConfig(config) - handshaker, err := boxstream.NewHandshaker(private, networkKey) + currentTimeProvider := adapters.NewCurrentTimeProvider() + handshaker, err := boxstream.NewHandshaker(private, networkKey, currentTimeProvider) if err != nil { return Service{}, err } @@ -282,10 +290,11 @@ func BuildService(contextContext context.Context, private identity.Private, conf if err != nil { return Service{}, err } - redeemInviteHandler := commands.NewRedeemInviteHandler(dialer, transactionProvider, networkKey, private, requestPubSub, marshaler, connectionIdGenerator, logger) + redeemInviteHandler := commands.NewRedeemInviteHandler(dialer, transactionProvider, networkKey, private, requestPubSub, marshaler, connectionIdGenerator, currentTimeProvider, logger) followHandler := commands.NewFollowHandler(transactionProvider, private, marshaler, logger) publishRawHandler := commands.NewPublishRawHandler(transactionProvider, private, logger) peerManagerConfig := extractPeerManagerConfigFromConfig(config) + tunnelDialer := tunnel.NewDialer(peerInitializer) sessionTracker := ebt.NewSessionTracker() messageHMAC := extractMessageHMACFromConfig(config) scuttlebutt := formats.NewScuttlebutt(marshaler, messageHMAC) @@ -318,34 +327,38 @@ func BuildService(contextContext context.Context, private identity.Private, conf hasHandler := replication2.NewHasHandler(filesystemStorage, readWantListRepository, blobsGetDownloader, blobDownloadedPubSub, logger) replicationManager := replication2.NewManager(readWantListRepository, filesystemStorage, hasHandler, logger) replicationReplicator := replication2.NewReplicator(replicationManager) - peerManager := domain.NewPeerManager(contextContext, peerManagerConfig, negotiator, replicationReplicator, dialer, logger) + peerRPCAdapter := rooms.NewPeerRPCAdapter(logger) + roomAttendantEventPubSub := pubsub.NewRoomAttendantEventPubSub() + scanner := rooms.NewScanner(peerRPCAdapter, peerRPCAdapter, roomAttendantEventPubSub, logger) + peerManager := domain.NewPeerManager(contextContext, peerManagerConfig, dialer, tunnelDialer, negotiator, replicationReplicator, scanner, logger) connectHandler := commands.NewConnectHandler(peerManager, logger) establishNewConnectionsHandler := commands.NewEstablishNewConnectionsHandler(peerManager) acceptNewPeerHandler := commands.NewAcceptNewPeerHandler(peerManager) processNewLocalDiscoveryHandler := commands.NewProcessNewLocalDiscoveryHandler(peerManager) createWantsHandler := commands.NewCreateWantsHandler(replicationManager) - currentTimeProvider := adapters.NewCurrentTimeProvider() downloadBlobHandler := commands.NewDownloadBlobHandler(transactionProvider, currentTimeProvider) createBlobHandler := commands.NewCreateBlobHandler(filesystemStorage) addToBanListHandler := commands.NewAddToBanListHandler(transactionProvider) removeFromBanListHandler := commands.NewRemoveFromBanListHandler(transactionProvider) roomsAliasRegisterHandler := commands.NewRoomsAliasRegisterHandler(dialer, private) roomsAliasRevokeHandler := commands.NewRoomsAliasRevokeHandler(dialer) + processRoomAttendantEventHandler := commands.NewProcessRoomAttendantEventHandler(peerManager) appCommands := app.Commands{ - RedeemInvite: redeemInviteHandler, - Follow: followHandler, - PublishRaw: publishRawHandler, - Connect: connectHandler, - EstablishNewConnections: establishNewConnectionsHandler, - AcceptNewPeer: acceptNewPeerHandler, - ProcessNewLocalDiscovery: processNewLocalDiscoveryHandler, - CreateWants: createWantsHandler, - DownloadBlob: downloadBlobHandler, - CreateBlob: createBlobHandler, - AddToBanList: addToBanListHandler, - RemoveFromBanList: removeFromBanListHandler, - RoomsAliasRegister: roomsAliasRegisterHandler, - RoomsAliasRevoke: roomsAliasRevokeHandler, + RedeemInvite: redeemInviteHandler, + Follow: followHandler, + PublishRaw: publishRawHandler, + Connect: connectHandler, + EstablishNewConnections: establishNewConnectionsHandler, + AcceptNewPeer: acceptNewPeerHandler, + ProcessNewLocalDiscovery: processNewLocalDiscoveryHandler, + CreateWants: createWantsHandler, + DownloadBlob: downloadBlobHandler, + CreateBlob: createBlobHandler, + AddToBanList: addToBanListHandler, + RemoveFromBanList: removeFromBanListHandler, + RoomsAliasRegister: roomsAliasRegisterHandler, + RoomsAliasRevoke: roomsAliasRevokeHandler, + ProcessRoomAttendantEvent: processRoomAttendantEventHandler, } readReceiveLogRepository := bolt.NewReadReceiveLogRepository(db, txRepositoriesFactory) receiveLogHandler := queries.NewReceiveLogHandler(readReceiveLogRepository) @@ -399,11 +412,12 @@ func BuildService(contextContext context.Context, private identity.Private, conf return Service{}, err } requestSubscriber := pubsub2.NewRequestSubscriber(requestPubSub, muxMux) + roomAttendantEventSubscriber := pubsub2.NewRoomAttendantEventSubscriber(roomAttendantEventPubSub, processRoomAttendantEventHandler, logger) advertiser, err := newAdvertiser(public, config) if err != nil { return Service{}, err } - service := NewService(application, listener, networkDiscoverer, connectionEstablisher, requestSubscriber, advertiser, messageBuffer, createHistoryStreamHandler) + service := NewService(application, listener, networkDiscoverer, connectionEstablisher, requestSubscriber, roomAttendantEventSubscriber, advertiser, messageBuffer, createHistoryStreamHandler) return service, nil } @@ -430,10 +444,12 @@ type TestAdapters struct { } type TestCommands struct { - RoomsAliasRegister *commands.RoomsAliasRegisterHandler - RoomsAliasRevoke *commands.RoomsAliasRevokeHandler + RoomsAliasRegister *commands.RoomsAliasRegisterHandler + RoomsAliasRevoke *commands.RoomsAliasRevokeHandler + ProcessRoomAttendantEvent *commands.ProcessRoomAttendantEventHandler - Dialer *mocks.DialerMock + PeerManager *mocks2.PeerManagerMock + Dialer *mocks.DialerMock } type TestQueries struct { @@ -442,7 +458,7 @@ type TestQueries struct { FeedRepository *mocks.FeedRepositoryMock MessagePubSub *mocks.MessagePubSubMock MessageRepository *mocks.MessageRepositoryMock - PeerManager *mocks.PeerManagerMock + PeerManager *mocks2.PeerManagerMock BlobStorage *mocks.BlobStorageMock ReceiveLogRepository *mocks.ReceiveLogRepositoryMock Dialer *mocks.DialerMock diff --git a/fixtures/fixtures.go b/fixtures/fixtures.go index c3ec7c69..c02e7f50 100644 --- a/fixtures/fixtures.go +++ b/fixtures/fixtures.go @@ -19,7 +19,7 @@ import ( "github.com/planetary-social/scuttlego/service/domain/feeds/message" "github.com/planetary-social/scuttlego/service/domain/identity" "github.com/planetary-social/scuttlego/service/domain/refs" - "github.com/planetary-social/scuttlego/service/domain/rooms" + "github.com/planetary-social/scuttlego/service/domain/rooms/aliases" "github.com/planetary-social/scuttlego/service/domain/transport/rpc" "github.com/planetary-social/scuttlego/service/domain/transport/rpc/transport" "github.com/sirupsen/logrus" @@ -137,8 +137,8 @@ func SomeString() string { return strconv.Itoa(SomeNonNegativeInt()) } -func SomeAlias() rooms.Alias { - return rooms.MustNewAlias("alias" + SomeString()) +func SomeAlias() aliases.Alias { + return aliases.MustNewAlias("alias" + SomeString()) } func SomeBytes() []byte { diff --git a/service/adapters/mocks/dialer.go b/service/adapters/mocks/dialer.go index 84b6ca8e..dd36b442 100644 --- a/service/adapters/mocks/dialer.go +++ b/service/adapters/mocks/dialer.go @@ -33,7 +33,7 @@ func (d *DialerMock) Dial(ctx context.Context, remote identity.Public, address n } func (d *DialerMock) MockPeer(remote identity.Public, address network.Address, connection transport.Connection) { - d.peers[d.key(remote, address)] = transport.NewPeer( + d.peers[d.key(remote, address)] = transport.MustNewPeer( remote, connection, ) diff --git a/service/adapters/mocks/peer_manager.go b/service/adapters/mocks/peer_manager.go deleted file mode 100644 index f36c3e57..00000000 --- a/service/adapters/mocks/peer_manager.go +++ /dev/null @@ -1,17 +0,0 @@ -package mocks - -import ( - "github.com/planetary-social/scuttlego/service/domain/transport" -) - -type PeerManagerMock struct { - PeersReturnValue []transport.Peer -} - -func NewPeerManagerMock() *PeerManagerMock { - return &PeerManagerMock{} -} - -func (p PeerManagerMock) Peers() []transport.Peer { - return p.PeersReturnValue -} diff --git a/service/adapters/pubsub/attendant_event_pubsub.go b/service/adapters/pubsub/attendant_event_pubsub.go new file mode 100644 index 00000000..5471549f --- /dev/null +++ b/service/adapters/pubsub/attendant_event_pubsub.go @@ -0,0 +1,35 @@ +package pubsub + +import ( + "context" + + "github.com/planetary-social/scuttlego/service/domain/rooms" + "github.com/planetary-social/scuttlego/service/domain/transport" +) + +type RoomAttendantEventPubSub struct { + pubsub *GoChannelPubSub[RoomAttendantEvent] +} + +func NewRoomAttendantEventPubSub() *RoomAttendantEventPubSub { + return &RoomAttendantEventPubSub{ + pubsub: NewGoChannelPubSub[RoomAttendantEvent](), + } +} + +func (p *RoomAttendantEventPubSub) PublishAttendantEvent(portal transport.Peer, event rooms.RoomAttendantsEvent) error { + p.pubsub.Publish(RoomAttendantEvent{ + Portal: portal, + Event: event, + }) + return nil +} + +func (p *RoomAttendantEventPubSub) SubscribeToAttendantEvents(ctx context.Context) <-chan RoomAttendantEvent { + return p.pubsub.Subscribe(ctx) +} + +type RoomAttendantEvent struct { + Portal transport.Peer + Event rooms.RoomAttendantsEvent +} diff --git a/service/app/application.go b/service/app/application.go index 15a5696d..a56f9a86 100644 --- a/service/app/application.go +++ b/service/app/application.go @@ -27,8 +27,9 @@ type Commands struct { AddToBanList *commands.AddToBanListHandler RemoveFromBanList *commands.RemoveFromBanListHandler - RoomsAliasRegister *commands.RoomsAliasRegisterHandler - RoomsAliasRevoke *commands.RoomsAliasRevokeHandler + RoomsAliasRegister *commands.RoomsAliasRegisterHandler + RoomsAliasRevoke *commands.RoomsAliasRevokeHandler + ProcessRoomAttendantEvent *commands.ProcessRoomAttendantEventHandler } type Queries struct { diff --git a/service/app/commands/common.go b/service/app/commands/common.go index ef20c012..54cccc0e 100644 --- a/service/app/commands/common.go +++ b/service/app/commands/common.go @@ -1,6 +1,7 @@ package commands import ( + "context" "time" "github.com/boreq/errors" @@ -24,6 +25,11 @@ type PeerManager interface { // duplicate connections to a single identity. Connect(remote identity.Public, address network.Address) error + // ConnectViaRoom instructs the peer manager that it should establish + // communications with the specified node using a room as a relay. Behaves + // like Connect. + ConnectViaRoom(portal transport.Peer, target identity.Public) error + // EstablishNewConnections instructs the peer manager that it is time to // establish new connections so that the specific connections quotas are // met. @@ -105,3 +111,8 @@ type BanListRepository interface { } var ErrBanListMappingNotFound = errors.New("ban list mapping not found") + +type Dialer interface { + DialWithInitializer(ctx context.Context, initializer network.ClientPeerInitializer, remote identity.Public, addr network.Address) (transport.Peer, error) + Dial(ctx context.Context, remote identity.Public, address network.Address) (transport.Peer, error) +} diff --git a/service/app/commands/handler_process_room_attendant_event.go b/service/app/commands/handler_process_room_attendant_event.go new file mode 100644 index 00000000..d0c1ff1d --- /dev/null +++ b/service/app/commands/handler_process_room_attendant_event.go @@ -0,0 +1,62 @@ +package commands + +import ( + "github.com/boreq/errors" + "github.com/planetary-social/scuttlego/service/domain/rooms" + "github.com/planetary-social/scuttlego/service/domain/transport" +) + +type ProcessRoomAttendantEvent struct { + portal transport.Peer + event rooms.RoomAttendantsEvent +} + +func NewProcessRoomAttendantEvent( + portal transport.Peer, + event rooms.RoomAttendantsEvent, +) (ProcessRoomAttendantEvent, error) { + if portal.IsZero() { + return ProcessRoomAttendantEvent{}, errors.New("zero value of portal") + } + if event.IsZero() { + return ProcessRoomAttendantEvent{}, errors.New("zero value of event") + } + return ProcessRoomAttendantEvent{ + portal: portal, + event: event, + }, nil +} + +func (e ProcessRoomAttendantEvent) Portal() transport.Peer { + return e.portal +} + +func (e ProcessRoomAttendantEvent) Event() rooms.RoomAttendantsEvent { + return e.event +} + +func (e ProcessRoomAttendantEvent) IsZero() bool { + return e.event.IsZero() +} + +type ProcessRoomAttendantEventHandler struct { + peerManager PeerManager +} + +func NewProcessRoomAttendantEventHandler(peerManager PeerManager) *ProcessRoomAttendantEventHandler { + return &ProcessRoomAttendantEventHandler{peerManager: peerManager} +} + +func (h *ProcessRoomAttendantEventHandler) Handle(cmd ProcessRoomAttendantEvent) error { + if cmd.IsZero() { + return errors.New("zero value of command") + } + + if cmd.Event().Typ() == rooms.RoomAttendantsEventTypeJoined { + if err := h.peerManager.ConnectViaRoom(cmd.Portal(), cmd.Event().Id().Identity()); err != nil { + return errors.Wrap(err, "failed to connect") + } + } + + return nil +} diff --git a/service/app/commands/handler_process_room_attendant_event_test.go b/service/app/commands/handler_process_room_attendant_event_test.go new file mode 100644 index 00000000..c6ed3958 --- /dev/null +++ b/service/app/commands/handler_process_room_attendant_event_test.go @@ -0,0 +1,96 @@ +package commands_test + +import ( + "testing" + + "github.com/planetary-social/scuttlego/di" + "github.com/planetary-social/scuttlego/fixtures" + "github.com/planetary-social/scuttlego/service/app/commands" + "github.com/planetary-social/scuttlego/service/domain/mocks" + "github.com/planetary-social/scuttlego/service/domain/rooms" + "github.com/planetary-social/scuttlego/service/domain/transport" + "github.com/stretchr/testify/require" +) + +func TestNewProcessRoomAttendantEvent_ValuesAreCorrectlySetAndReturnedByGetters(t *testing.T) { + ctx := fixtures.TestContext(t) + conn := mocks.NewConnectionMock(ctx) + + portal, err := transport.NewPeer(fixtures.SomePublicIdentity(), conn) + require.NoError(t, err) + + event, err := rooms.NewRoomAttendantsEvent(rooms.RoomAttendantsEventTypeJoined, fixtures.SomeRefIdentity()) + require.NoError(t, err) + + cmd, err := commands.NewProcessRoomAttendantEvent(portal, event) + require.NoError(t, err) + + require.Equal(t, portal, cmd.Portal()) + require.Equal(t, event, cmd.Event()) + require.False(t, cmd.IsZero()) +} + +func TestProcessRoomAttendantEventHandler_ReturnsErrorOnZeroValueOfCommand(t *testing.T) { + tc, err := di.BuildTestCommands(t) + require.NoError(t, err) + + err = tc.ProcessRoomAttendantEvent.Handle(commands.ProcessRoomAttendantEvent{}) + require.EqualError(t, err, "zero value of command") +} + +func TestProcessRoomAttendantEventHandler_CallsPeerManagerConnectViaRoomForSpecificEventTypes(t *testing.T) { + testCases := []struct { + Name string + EventType rooms.RoomAttendantsEventType + ShouldCallConnectViaRoom bool + }{ + { + Name: "joined", + EventType: rooms.RoomAttendantsEventTypeJoined, + ShouldCallConnectViaRoom: true, + }, + { + Name: "left", + EventType: rooms.RoomAttendantsEventTypeLeft, + ShouldCallConnectViaRoom: false, + }, + } + + for _, testCase := range testCases { + t.Run(testCase.Name, func(t *testing.T) { + tc, err := di.BuildTestCommands(t) + require.NoError(t, err) + + ctx := fixtures.TestContext(t) + conn := mocks.NewConnectionMock(ctx) + + portal, err := transport.NewPeer(fixtures.SomePublicIdentity(), conn) + require.NoError(t, err) + + target := fixtures.SomeRefIdentity() + + event, err := rooms.NewRoomAttendantsEvent(testCase.EventType, target) + require.NoError(t, err) + + cmd, err := commands.NewProcessRoomAttendantEvent(portal, event) + require.NoError(t, err) + + err = tc.ProcessRoomAttendantEvent.Handle(cmd) + require.NoError(t, err) + + if testCase.ShouldCallConnectViaRoom { + require.Equal(t, + []mocks.PeerManagerConnectViaRoomCall{ + { + Portal: portal, + Target: target.Identity(), + }, + }, + tc.PeerManager.ConnectViaRoomCalls(), + ) + } else { + require.Empty(t, tc.PeerManager.ConnectViaRoomCalls()) + } + }) + } +} diff --git a/service/app/commands/handler_redeem_invite.go b/service/app/commands/handler_redeem_invite.go index 3879eb85..78b13fcc 100644 --- a/service/app/commands/handler_redeem_invite.go +++ b/service/app/commands/handler_redeem_invite.go @@ -12,18 +12,12 @@ import ( "github.com/planetary-social/scuttlego/service/domain/identity" "github.com/planetary-social/scuttlego/service/domain/invites" "github.com/planetary-social/scuttlego/service/domain/messages" - "github.com/planetary-social/scuttlego/service/domain/network" "github.com/planetary-social/scuttlego/service/domain/refs" "github.com/planetary-social/scuttlego/service/domain/transport" "github.com/planetary-social/scuttlego/service/domain/transport/boxstream" "github.com/planetary-social/scuttlego/service/domain/transport/rpc" ) -type Dialer interface { - DialWithInitializer(ctx context.Context, initializer network.ClientPeerInitializer, remote identity.Public, addr network.Address) (transport.Peer, error) - Dial(ctx context.Context, remote identity.Public, address network.Address) (transport.Peer, error) -} - type RedeemInvite struct { Invite invites.Invite } @@ -36,6 +30,7 @@ type RedeemInviteHandler struct { requestHandler rpc.RequestHandler marshaler formats.Marshaler connectionIdGenerator *rpc.ConnectionIdGenerator + currentTimeProvider CurrentTimeProvider logger logging.Logger } @@ -47,6 +42,7 @@ func NewRedeemInviteHandler( requestHandler rpc.RequestHandler, marshaler formats.Marshaler, connectionIdGenerator *rpc.ConnectionIdGenerator, + currentTimeProvider CurrentTimeProvider, logger logging.Logger, ) *RedeemInviteHandler { return &RedeemInviteHandler{ @@ -57,6 +53,7 @@ func NewRedeemInviteHandler( requestHandler: requestHandler, marshaler: marshaler, connectionIdGenerator: connectionIdGenerator, + currentTimeProvider: currentTimeProvider, logger: logger.New("follow_handler"), } } @@ -150,7 +147,7 @@ func (h *RedeemInviteHandler) dial(ctx context.Context, cmd RedeemInvite) (trans return transport.Peer{}, errors.Wrap(err, "could not create a private identity") } - handshaker, err := boxstream.NewHandshaker(local, h.networkKey) + handshaker, err := boxstream.NewHandshaker(local, h.networkKey, h.currentTimeProvider) if err != nil { return transport.Peer{}, errors.Wrap(err, "could not create a handshaker") } diff --git a/service/app/commands/handler_rooms_alias_register.go b/service/app/commands/handler_rooms_alias_register.go index 32f32ea2..20f5df9d 100644 --- a/service/app/commands/handler_rooms_alias_register.go +++ b/service/app/commands/handler_rooms_alias_register.go @@ -9,20 +9,20 @@ import ( "github.com/planetary-social/scuttlego/service/domain/messages" "github.com/planetary-social/scuttlego/service/domain/network" "github.com/planetary-social/scuttlego/service/domain/refs" - "github.com/planetary-social/scuttlego/service/domain/rooms" + "github.com/planetary-social/scuttlego/service/domain/rooms/aliases" "github.com/planetary-social/scuttlego/service/domain/transport/rpc" ) type RoomsAliasRegister struct { room refs.Identity address network.Address - alias rooms.Alias + alias aliases.Alias } func NewRoomsAliasRegister( room refs.Identity, address network.Address, - alias rooms.Alias, + alias aliases.Alias, ) (RoomsAliasRegister, error) { if room.IsZero() { return RoomsAliasRegister{}, errors.New("zero value of room") @@ -51,7 +51,7 @@ func (r RoomsAliasRegister) Address() network.Address { return r.address } -func (r RoomsAliasRegister) Alias() rooms.Alias { +func (r RoomsAliasRegister) Alias() aliases.Alias { return r.alias } @@ -74,40 +74,40 @@ func NewRoomsAliasRegisterHandler( } } -func (h *RoomsAliasRegisterHandler) Handle(ctx context.Context, cmd RoomsAliasRegister) (rooms.AliasEndpointURL, error) { +func (h *RoomsAliasRegisterHandler) Handle(ctx context.Context, cmd RoomsAliasRegister) (aliases.AliasEndpointURL, error) { if cmd.IsZero() { - return rooms.AliasEndpointURL{}, errors.New("zero value of command") + return aliases.AliasEndpointURL{}, errors.New("zero value of command") } user, err := refs.NewIdentityFromPublic(h.local.Public()) if err != nil { - return rooms.AliasEndpointURL{}, errors.Wrap(err, "failed to create user ref") + return aliases.AliasEndpointURL{}, errors.Wrap(err, "failed to create user ref") } - msg, err := rooms.NewRegistrationMessage(cmd.Alias(), user, cmd.Room()) + msg, err := aliases.NewRegistrationMessage(cmd.Alias(), user, cmd.Room()) if err != nil { - return rooms.AliasEndpointURL{}, errors.Wrap(err, "error creating a registration") + return aliases.AliasEndpointURL{}, errors.Wrap(err, "error creating a registration") } - signature, err := rooms.NewRegistrationSignature(msg, h.local) + signature, err := aliases.NewRegistrationSignature(msg, h.local) if err != nil { - return rooms.AliasEndpointURL{}, errors.Wrap(err, "error creating a signature") + return aliases.AliasEndpointURL{}, errors.Wrap(err, "error creating a signature") } response, err := h.registerAlias(ctx, cmd, signature) if err != nil { - return rooms.AliasEndpointURL{}, errors.Wrap(err, "could not contact the pub and redeem the invite") + return aliases.AliasEndpointURL{}, errors.Wrap(err, "could not contact the pub and redeem the invite") } registerAliasResponse, err := messages.NewRoomRegisterAliasResponseFromBytes(response.Bytes()) if err != nil { - return rooms.AliasEndpointURL{}, errors.Wrap(err, "error creating the response") + return aliases.AliasEndpointURL{}, errors.Wrap(err, "error creating the response") } return registerAliasResponse.AliasEndpointURL(), nil } -func (h *RoomsAliasRegisterHandler) registerAlias(ctx context.Context, cmd RoomsAliasRegister, signature rooms.RegistrationSignature) (*rpc.Response, error) { +func (h *RoomsAliasRegisterHandler) registerAlias(ctx context.Context, cmd RoomsAliasRegister, signature aliases.RegistrationSignature) (*rpc.Response, error) { ctx, cancel := context.WithTimeout(ctx, 30*time.Second) defer cancel() @@ -139,8 +139,8 @@ func (h *RoomsAliasRegisterHandler) registerAlias(ctx context.Context, cmd Rooms } func (h *RoomsAliasRegisterHandler) createRequest( - alias rooms.Alias, - signature rooms.RegistrationSignature, + alias aliases.Alias, + signature aliases.RegistrationSignature, ) (*rpc.Request, error) { args, err := messages.NewRoomRegisterAliasArguments(alias, signature) if err != nil { diff --git a/service/app/commands/handler_rooms_alias_register_test.go b/service/app/commands/handler_rooms_alias_register_test.go index 3790d42e..652f281e 100644 --- a/service/app/commands/handler_rooms_alias_register_test.go +++ b/service/app/commands/handler_rooms_alias_register_test.go @@ -9,8 +9,8 @@ import ( "github.com/planetary-social/scuttlego/fixtures" "github.com/planetary-social/scuttlego/service/app/commands" "github.com/planetary-social/scuttlego/service/domain/messages" + "github.com/planetary-social/scuttlego/service/domain/mocks" "github.com/planetary-social/scuttlego/service/domain/network" - "github.com/planetary-social/scuttlego/service/domain/transport/mocks" "github.com/planetary-social/scuttlego/service/domain/transport/rpc" "github.com/stretchr/testify/require" ) diff --git a/service/app/commands/handler_rooms_alias_revoke.go b/service/app/commands/handler_rooms_alias_revoke.go index fc9d5f55..322b3fab 100644 --- a/service/app/commands/handler_rooms_alias_revoke.go +++ b/service/app/commands/handler_rooms_alias_revoke.go @@ -9,20 +9,20 @@ import ( "github.com/planetary-social/scuttlego/service/domain/messages" "github.com/planetary-social/scuttlego/service/domain/network" "github.com/planetary-social/scuttlego/service/domain/refs" - "github.com/planetary-social/scuttlego/service/domain/rooms" + "github.com/planetary-social/scuttlego/service/domain/rooms/aliases" "github.com/planetary-social/scuttlego/service/domain/transport/rpc" ) type RoomsAliasRevoke struct { room refs.Identity address network.Address - alias rooms.Alias + alias aliases.Alias } func NewRoomsAliasRevoke( room refs.Identity, address network.Address, - alias rooms.Alias, + alias aliases.Alias, ) (RoomsAliasRevoke, error) { if room.IsZero() { return RoomsAliasRevoke{}, errors.New("zero value of room") @@ -51,7 +51,7 @@ func (r RoomsAliasRevoke) Address() network.Address { return r.address } -func (r RoomsAliasRevoke) Alias() rooms.Alias { +func (r RoomsAliasRevoke) Alias() aliases.Alias { return r.alias } @@ -107,7 +107,7 @@ func (h *RoomsAliasRevokeHandler) Handle(ctx context.Context, cmd RoomsAliasRevo } func (h *RoomsAliasRevokeHandler) createRequest( - alias rooms.Alias, + alias aliases.Alias, ) (*rpc.Request, error) { args, err := messages.NewRoomRevokeAliasArguments(alias) if err != nil { diff --git a/service/app/commands/handler_rooms_alias_revoke_test.go b/service/app/commands/handler_rooms_alias_revoke_test.go index 2f0c6f08..642df8f8 100644 --- a/service/app/commands/handler_rooms_alias_revoke_test.go +++ b/service/app/commands/handler_rooms_alias_revoke_test.go @@ -9,8 +9,8 @@ import ( "github.com/planetary-social/scuttlego/fixtures" "github.com/planetary-social/scuttlego/service/app/commands" "github.com/planetary-social/scuttlego/service/domain/messages" + "github.com/planetary-social/scuttlego/service/domain/mocks" "github.com/planetary-social/scuttlego/service/domain/network" - "github.com/planetary-social/scuttlego/service/domain/transport/mocks" "github.com/planetary-social/scuttlego/service/domain/transport/rpc" "github.com/stretchr/testify/require" ) diff --git a/service/app/queries/handler_rooms_list_aliases.go b/service/app/queries/handler_rooms_list_aliases.go index 9b93576a..5650b288 100644 --- a/service/app/queries/handler_rooms_list_aliases.go +++ b/service/app/queries/handler_rooms_list_aliases.go @@ -9,7 +9,7 @@ import ( "github.com/planetary-social/scuttlego/service/domain/messages" "github.com/planetary-social/scuttlego/service/domain/network" "github.com/planetary-social/scuttlego/service/domain/refs" - "github.com/planetary-social/scuttlego/service/domain/rooms" + "github.com/planetary-social/scuttlego/service/domain/rooms/aliases" "github.com/planetary-social/scuttlego/service/domain/transport/rpc" ) @@ -68,7 +68,7 @@ func NewRoomsListAliasesHandler( }, nil } -func (h *RoomsListAliasesHandler) Handle(ctx context.Context, query RoomsListAliases) ([]rooms.Alias, error) { +func (h *RoomsListAliasesHandler) Handle(ctx context.Context, query RoomsListAliases) ([]aliases.Alias, error) { if query.IsZero() { return nil, errors.New("zero value of command") } @@ -100,7 +100,7 @@ func (h *RoomsListAliasesHandler) Handle(ctx context.Context, query RoomsListAli return nil, errors.Wrap(err, "received an error") } - listAliasesResponse, err := messages.NewRoomsListAliasesResponse(response.Value.Bytes()) + listAliasesResponse, err := messages.NewRoomListAliasesResponseFromBytes(response.Value.Bytes()) if err != nil { return nil, errors.Wrap(err, "error creating the response") } diff --git a/service/app/queries/handler_rooms_list_aliases_test.go b/service/app/queries/handler_rooms_list_aliases_test.go index 64634994..ae96ef6c 100644 --- a/service/app/queries/handler_rooms_list_aliases_test.go +++ b/service/app/queries/handler_rooms_list_aliases_test.go @@ -9,9 +9,9 @@ import ( "github.com/planetary-social/scuttlego/fixtures" "github.com/planetary-social/scuttlego/service/app/queries" "github.com/planetary-social/scuttlego/service/domain/messages" + "github.com/planetary-social/scuttlego/service/domain/mocks" "github.com/planetary-social/scuttlego/service/domain/network" - "github.com/planetary-social/scuttlego/service/domain/rooms" - "github.com/planetary-social/scuttlego/service/domain/transport/mocks" + "github.com/planetary-social/scuttlego/service/domain/rooms/aliases" "github.com/planetary-social/scuttlego/service/domain/transport/rpc" "github.com/stretchr/testify/require" ) @@ -54,14 +54,14 @@ func TestRoomsListAliasesHandler(t *testing.T) { ) require.NoError(t, err) - aliases, err := c.Queries.RoomsListAliases.Handle(ctx, query) + result, err := c.Queries.RoomsListAliases.Handle(ctx, query) require.NoError(t, err) require.Equal(t, - []rooms.Alias{ - rooms.MustNewAlias("alias1"), - rooms.MustNewAlias("alias2"), + []aliases.Alias{ + aliases.MustNewAlias("alias1"), + aliases.MustNewAlias("alias2"), }, - aliases, + result, ) } diff --git a/service/app/queries/status_test.go b/service/app/queries/status_test.go index 2a4956a5..c2d41792 100644 --- a/service/app/queries/status_test.go +++ b/service/app/queries/status_test.go @@ -6,6 +6,7 @@ import ( "github.com/planetary-social/scuttlego/di" "github.com/planetary-social/scuttlego/fixtures" "github.com/planetary-social/scuttlego/service/app/queries" + "github.com/planetary-social/scuttlego/service/domain/mocks" "github.com/planetary-social/scuttlego/service/domain/transport" "github.com/stretchr/testify/require" ) @@ -14,6 +15,8 @@ func TestStatus(t *testing.T) { a, err := di.BuildTestQueries(t) require.NoError(t, err) + ctx := fixtures.TestContext(t) + expectedMessageCount := 123 expectedFeedCount := 456 @@ -21,9 +24,9 @@ func TestStatus(t *testing.T) { a.MessageRepository.CountReturnValue = expectedMessageCount a.FeedRepository.CountReturnValue = expectedFeedCount - a.PeerManager.PeersReturnValue = []transport.Peer{ - transport.NewPeer(remote, nil), - } + a.PeerManager.MockPeers([]transport.Peer{ + transport.MustNewPeer(remote, mocks.NewConnectionMock(ctx)), + }) result, err := a.Queries.Status.Handle() require.NoError(t, err) diff --git a/service/domain/blobs/replication/has_handler_test.go b/service/domain/blobs/replication/has_handler_test.go index 5c0d548e..5256655e 100644 --- a/service/domain/blobs/replication/has_handler_test.go +++ b/service/domain/blobs/replication/has_handler_test.go @@ -12,6 +12,7 @@ import ( "github.com/planetary-social/scuttlego/service/app/queries" "github.com/planetary-social/scuttlego/service/domain/blobs" "github.com/planetary-social/scuttlego/service/domain/blobs/replication" + domainMocks "github.com/planetary-social/scuttlego/service/domain/mocks" "github.com/planetary-social/scuttlego/service/domain/refs" "github.com/planetary-social/scuttlego/service/domain/transport" "github.com/stretchr/testify/assert" @@ -71,7 +72,7 @@ func TestHasHandlerTriggersDownloader(t *testing.T) { h := newTestHasHandler() ctx := fixtures.TestContext(t) - peer := transport.NewPeer(fixtures.SomePublicIdentity(), nil) + peer := transport.MustNewPeer(fixtures.SomePublicIdentity(), domainMocks.NewConnectionMock(ctx)) blob := fixtures.SomeRefBlob() if testCase.InWantList { @@ -133,7 +134,7 @@ func TestHasHandlerRemovesElementFromWantListIfItIsAlreadyInStorage(t *testing.T h := newTestHasHandler() ctx := fixtures.TestContext(t) - peer := transport.NewPeer(fixtures.SomePublicIdentity(), nil) + peer := transport.MustNewPeer(fixtures.SomePublicIdentity(), domainMocks.NewConnectionMock(ctx)) blob := fixtures.SomeRefBlob() h.WantList.AddBlob(blob) diff --git a/service/domain/blobs/replication/manager_test.go b/service/domain/blobs/replication/manager_test.go index 788ad5a8..4077de11 100644 --- a/service/domain/blobs/replication/manager_test.go +++ b/service/domain/blobs/replication/manager_test.go @@ -12,6 +12,7 @@ import ( "github.com/planetary-social/scuttlego/service/domain/blobs" "github.com/planetary-social/scuttlego/service/domain/blobs/replication" "github.com/planetary-social/scuttlego/service/domain/messages" + domainmocks "github.com/planetary-social/scuttlego/service/domain/mocks" "github.com/planetary-social/scuttlego/service/domain/refs" "github.com/planetary-social/scuttlego/service/domain/transport" "github.com/planetary-social/scuttlego/service/domain/transport/rpc" @@ -53,7 +54,7 @@ func TestManagerTriggersDownloaderAfterReceivingHas(t *testing.T) { ch := make(chan messages.BlobWithSizeOrWantDistance) - peer := transport.NewPeer(fixtures.SomePublicIdentity(), nil) + peer := transport.MustNewPeer(fixtures.SomePublicIdentity(), domainmocks.NewConnectionMock(ctx)) err := m.Manager.HandleOutgoingCreateWantsRequest(ctx, ch, peer) require.NoError(t, err) diff --git a/service/domain/blobs/replication/wants_test.go b/service/domain/blobs/replication/wants_test.go index f4efcc9e..a4b0b597 100644 --- a/service/domain/blobs/replication/wants_test.go +++ b/service/domain/blobs/replication/wants_test.go @@ -11,6 +11,7 @@ import ( "github.com/planetary-social/scuttlego/service/domain/blobs" "github.com/planetary-social/scuttlego/service/domain/blobs/replication" "github.com/planetary-social/scuttlego/service/domain/messages" + domainmocks "github.com/planetary-social/scuttlego/service/domain/mocks" "github.com/planetary-social/scuttlego/service/domain/transport" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" @@ -21,7 +22,7 @@ func TestRepliesToWantsWithHas(t *testing.T) { ctx := fixtures.TestContext(t) outgoingCh := make(chan messages.BlobWithSizeOrWantDistance) - peer := transport.NewPeer(fixtures.SomePublicIdentity(), nil) + peer := transport.MustNewPeer(fixtures.SomePublicIdentity(), domainmocks.NewConnectionMock(ctx)) p.WantsProcess.AddOutgoing(ctx, outgoingCh, peer) incomingCh := make(chan messages.BlobWithSizeOrWantDistance) @@ -52,7 +53,7 @@ func TestRepliesToWantsWithHasIfIncomingChannelConnectedLater(t *testing.T) { ctx := fixtures.TestContext(t) outgoingCh := make(chan messages.BlobWithSizeOrWantDistance) - peer := transport.NewPeer(fixtures.SomePublicIdentity(), nil) + peer := transport.MustNewPeer(fixtures.SomePublicIdentity(), domainmocks.NewConnectionMock(ctx)) p.WantsProcess.AddOutgoing(ctx, outgoingCh, peer) blobId := fixtures.SomeRefBlob() @@ -82,7 +83,7 @@ func TestPassesHasToDownloader(t *testing.T) { defer cancel() outgoingCh := make(chan messages.BlobWithSizeOrWantDistance) - peer := transport.NewPeer(fixtures.SomePublicIdentity(), nil) + peer := transport.MustNewPeer(fixtures.SomePublicIdentity(), domainmocks.NewConnectionMock(ctx)) p.WantsProcess.AddOutgoing(ctx, outgoingCh, peer) blobId := fixtures.SomeRefBlob() diff --git a/service/domain/messages/blobs_createWants.go b/service/domain/messages/blobs_createWants.go index 80c8e932..ee980553 100644 --- a/service/domain/messages/blobs_createWants.go +++ b/service/domain/messages/blobs_createWants.go @@ -12,8 +12,10 @@ import ( ) var ( - BlobsCreateWantsProcedureName = rpc.MustNewProcedureName([]string{"blobs", "createWants"}) - BlobsCreateWantsProcedure = rpc.MustNewProcedure(BlobsCreateWantsProcedureName, rpc.ProcedureTypeSource) + BlobsCreateWantsProcedure = rpc.MustNewProcedure( + rpc.MustNewProcedureName([]string{"blobs", "createWants"}), + rpc.ProcedureTypeSource, + ) ) func NewBlobsCreateWants() (*rpc.Request, error) { diff --git a/service/domain/messages/blobs_get.go b/service/domain/messages/blobs_get.go index 16cb2f55..d34c6ba8 100644 --- a/service/domain/messages/blobs_get.go +++ b/service/domain/messages/blobs_get.go @@ -11,8 +11,10 @@ import ( ) var ( - BlobsGetProcedureName = rpc.MustNewProcedureName([]string{"blobs", "get"}) - BlobsGetProcedure = rpc.MustNewProcedure(BlobsGetProcedureName, rpc.ProcedureTypeSource) + BlobsGetProcedure = rpc.MustNewProcedure( + rpc.MustNewProcedureName([]string{"blobs", "get"}), + rpc.ProcedureTypeSource, + ) ) func NewBlobsGet(arguments BlobsGetArguments) (*rpc.Request, error) { diff --git a/service/domain/messages/create_history_stream.go b/service/domain/messages/create_history_stream.go index 9f395390..c107f5a1 100644 --- a/service/domain/messages/create_history_stream.go +++ b/service/domain/messages/create_history_stream.go @@ -11,8 +11,10 @@ import ( ) var ( - CreateHistoryStreamProcedureName = rpc.MustNewProcedureName([]string{"createHistoryStream"}) - CreateHistoryStreamProcedure = rpc.MustNewProcedure(CreateHistoryStreamProcedureName, rpc.ProcedureTypeSource) + CreateHistoryStreamProcedure = rpc.MustNewProcedure( + rpc.MustNewProcedureName([]string{"createHistoryStream"}), + rpc.ProcedureTypeSource, + ) ) func NewCreateHistoryStream(arguments CreateHistoryStreamArguments) (*rpc.Request, error) { diff --git a/service/domain/messages/ebt_replicate.go b/service/domain/messages/ebt_replicate.go index fa0a6c95..9d7ca7f1 100644 --- a/service/domain/messages/ebt_replicate.go +++ b/service/domain/messages/ebt_replicate.go @@ -10,8 +10,10 @@ import ( ) var ( - EbtReplicateProcedureName = rpc.MustNewProcedureName([]string{"ebt", "replicate"}) - EbtReplicateProcedure = rpc.MustNewProcedure(EbtReplicateProcedureName, rpc.ProcedureTypeDuplex) + EbtReplicateProcedure = rpc.MustNewProcedure( + rpc.MustNewProcedureName([]string{"ebt", "replicate"}), + rpc.ProcedureTypeDuplex, + ) ) func NewEbtReplicate(arguments EbtReplicateArguments) (*rpc.Request, error) { diff --git a/service/domain/messages/invite_use.go b/service/domain/messages/invite_use.go index f661a0b9..6a1b048b 100644 --- a/service/domain/messages/invite_use.go +++ b/service/domain/messages/invite_use.go @@ -9,8 +9,10 @@ import ( ) var ( - InviteUseProcedureName = rpc.MustNewProcedureName([]string{"invite", "use"}) - InviteUseProcedure = rpc.MustNewProcedure(InviteUseProcedureName, rpc.ProcedureTypeAsync) + InviteUseProcedure = rpc.MustNewProcedure( + rpc.MustNewProcedureName([]string{"invite", "use"}), + rpc.ProcedureTypeAsync, + ) ) func NewInviteUse(arguments InviteUseArguments) (*rpc.Request, error) { diff --git a/service/domain/messages/room_attendants.go b/service/domain/messages/room_attendants.go new file mode 100644 index 00000000..7bcdded5 --- /dev/null +++ b/service/domain/messages/room_attendants.go @@ -0,0 +1,124 @@ +package messages + +import ( + "encoding/json" + "fmt" + + "github.com/boreq/errors" + "github.com/planetary-social/scuttlego/service/domain/refs" + "github.com/planetary-social/scuttlego/service/domain/transport/rpc" +) + +var ( + RoomAttendantsProcedure = rpc.MustNewProcedure( + rpc.MustNewProcedureName([]string{"room", "attendants"}), + rpc.ProcedureTypeSource, + ) +) + +func NewRoomAttendants() (*rpc.Request, error) { + return rpc.NewRequest( + RoomAttendantsProcedure.Name(), + RoomAttendantsProcedure.Typ(), + []byte("[]"), + ) +} + +type RoomAttendantsResponseState struct { + ids []refs.Identity +} + +func NewRoomAttendantsResponseStateFromBytes(b []byte) (RoomAttendantsResponseState, error) { + var transport roomAttendantsResponseStateTransport + if err := json.Unmarshal(b, &transport); err != nil { + return RoomAttendantsResponseState{}, errors.Wrap(err, "json unmarshal failed") + } + + if transport.Type != "state" { + return RoomAttendantsResponseState{}, errors.New("invalid response type") + } + + var refsSlice []refs.Identity + + for _, refString := range transport.Ids { + ref, err := refs.NewIdentity(refString) + if err != nil { + return RoomAttendantsResponseState{}, errors.Wrap(err, "error creating a ref") + } + + refsSlice = append(refsSlice, ref) + } + + return RoomAttendantsResponseState{ + ids: refsSlice, + }, nil +} + +func (r RoomAttendantsResponseState) Ids() []refs.Identity { + return r.ids +} + +type RoomAttendantsResponseJoinedOrLeft struct { + typ RoomAttendantsResponseType + id refs.Identity +} + +func NewRoomAttendantsResponseJoinedOrLeftFromBytes(b []byte) (RoomAttendantsResponseJoinedOrLeft, error) { + var transport roomAttendantsResponseJoinedOrLeftTransport + if err := json.Unmarshal(b, &transport); err != nil { + return RoomAttendantsResponseJoinedOrLeft{}, errors.Wrap(err, "json unmarshal failed") + } + + typ, err := decodeRoomAttendantsResponseType(transport.Type) + if err != nil { + return RoomAttendantsResponseJoinedOrLeft{}, errors.Wrap(err, "error decoding response type") + } + + ref, err := refs.NewIdentity(transport.Id) + if err != nil { + return RoomAttendantsResponseJoinedOrLeft{}, errors.Wrap(err, "error creating a ref") + } + + return RoomAttendantsResponseJoinedOrLeft{ + typ: typ, + id: ref, + }, nil +} + +func (r RoomAttendantsResponseJoinedOrLeft) Typ() RoomAttendantsResponseType { + return r.typ +} + +func (r RoomAttendantsResponseJoinedOrLeft) Id() refs.Identity { + return r.id +} + +type roomAttendantsResponseStateTransport struct { + Type string `json:"type"` + Ids []string `json:"ids"` +} + +type roomAttendantsResponseJoinedOrLeftTransport struct { + Type string `json:"type"` + Id string `json:"id"` +} + +type RoomAttendantsResponseType struct { + s string +} + +var ( + RoomAttendantsResponseTypeJoined = RoomAttendantsResponseType{"joined"} + RoomAttendantsResponseTypeLeft = RoomAttendantsResponseType{"left"} +) + +func decodeRoomAttendantsResponseType(s string) (RoomAttendantsResponseType, error) { + switch s { + case "joined": + return RoomAttendantsResponseTypeJoined, nil + case "left": + return RoomAttendantsResponseTypeLeft, nil + default: + return RoomAttendantsResponseType{}, fmt.Errorf("unknown type: '%s'", s) + } +} diff --git a/service/domain/messages/room_attendants_test.go b/service/domain/messages/room_attendants_test.go new file mode 100644 index 00000000..ef24ca2a --- /dev/null +++ b/service/domain/messages/room_attendants_test.go @@ -0,0 +1,82 @@ +package messages_test + +import ( + "encoding/json" + "testing" + + "github.com/planetary-social/scuttlego/service/domain/messages" + "github.com/planetary-social/scuttlego/service/domain/refs" + "github.com/planetary-social/scuttlego/service/domain/transport/rpc" + "github.com/stretchr/testify/require" +) + +func TestNewRoomAttendants(t *testing.T) { + req, err := messages.NewRoomAttendants() + require.NoError(t, err) + require.Equal(t, rpc.ProcedureTypeSource, req.Type()) + require.Equal(t, rpc.MustNewProcedureName([]string{"room", "attendants"}), req.Name()) + require.Equal(t, json.RawMessage("[]"), req.Arguments()) +} + +func TestNewRoomAttendantsResponseStateFromBytes(t *testing.T) { + s := ` +{ + "type":"state", + "ids": [ + "@Uv38ByGCZU8WP18PmmIdcpVmx00QA3xNe7sEB9Hixkk=.ed25519", + "@gYVa2GgdDYbR6R4AFnk5y2aU0sQirNIIoAcpOUh/aZk=.ed25519" + ] +} +` + resp, err := messages.NewRoomAttendantsResponseStateFromBytes([]byte(s)) + require.NoError(t, err) + + require.Equal(t, + []refs.Identity{ + refs.MustNewIdentity("@Uv38ByGCZU8WP18PmmIdcpVmx00QA3xNe7sEB9Hixkk=.ed25519"), + refs.MustNewIdentity("@gYVa2GgdDYbR6R4AFnk5y2aU0sQirNIIoAcpOUh/aZk=.ed25519"), + }, + resp.Ids(), + ) +} + +func TestNewRoomAttendantsResponseJoinedOrLeftFromBytes(t *testing.T) { + testCases := []struct { + Name string + String string + ExpectedTyp messages.RoomAttendantsResponseType + ExpectedId refs.Identity + }{ + { + Name: "joined", + String: ` +{ + "type":"joined", + "id": "@Uv38ByGCZU8WP18PmmIdcpVmx00QA3xNe7sEB9Hixkk=.ed25519" +} +`, + ExpectedTyp: messages.RoomAttendantsResponseTypeJoined, + ExpectedId: refs.MustNewIdentity("@Uv38ByGCZU8WP18PmmIdcpVmx00QA3xNe7sEB9Hixkk=.ed25519"), + }, + { + Name: "left", + String: ` +{ + "type":"left", + "id": "@Uv38ByGCZU8WP18PmmIdcpVmx00QA3xNe7sEB9Hixkk=.ed25519" +} +`, + ExpectedTyp: messages.RoomAttendantsResponseTypeLeft, + ExpectedId: refs.MustNewIdentity("@Uv38ByGCZU8WP18PmmIdcpVmx00QA3xNe7sEB9Hixkk=.ed25519"), + }, + } + + for _, testCase := range testCases { + t.Run(testCase.Name, func(t *testing.T) { + resp, err := messages.NewRoomAttendantsResponseJoinedOrLeftFromBytes([]byte(testCase.String)) + require.NoError(t, err) + require.Equal(t, testCase.ExpectedTyp, resp.Typ()) + require.Equal(t, testCase.ExpectedId, resp.Id()) + }) + } +} diff --git a/service/domain/messages/room_listAliases.go b/service/domain/messages/room_listAliases.go index f4cabfa2..7da7b369 100644 --- a/service/domain/messages/room_listAliases.go +++ b/service/domain/messages/room_listAliases.go @@ -5,7 +5,7 @@ import ( "github.com/boreq/errors" "github.com/planetary-social/scuttlego/service/domain/refs" - "github.com/planetary-social/scuttlego/service/domain/rooms" + "github.com/planetary-social/scuttlego/service/domain/rooms/aliases" "github.com/planetary-social/scuttlego/service/domain/transport/rpc" ) @@ -52,27 +52,27 @@ func (i RoomListAliasesArguments) MarshalJSON() ([]byte, error) { } type RoomListAliasesResponse struct { - aliases []rooms.Alias + aliases []aliases.Alias } -func NewRoomsListAliasesResponse(b []byte) (RoomListAliasesResponse, error) { +func NewRoomListAliasesResponseFromBytes(b []byte) (RoomListAliasesResponse, error) { var aliasesAsStrings []string if err := json.Unmarshal(b, &aliasesAsStrings); err != nil { return RoomListAliasesResponse{}, errors.Wrap(err, "json unmarshal failed") } - var aliases []rooms.Alias + var aliasesSlice []aliases.Alias for _, aliasString := range aliasesAsStrings { - alias, err := rooms.NewAlias(aliasString) + alias, err := aliases.NewAlias(aliasString) if err != nil { return RoomListAliasesResponse{}, errors.Wrap(err, "error creating an alias") } - aliases = append(aliases, alias) + aliasesSlice = append(aliasesSlice, alias) } - return RoomListAliasesResponse{aliases: aliases}, nil + return RoomListAliasesResponse{aliases: aliasesSlice}, nil } -func (r RoomListAliasesResponse) Aliases() []rooms.Alias { +func (r RoomListAliasesResponse) Aliases() []aliases.Alias { return r.aliases } diff --git a/service/domain/messages/room_listAliases_test.go b/service/domain/messages/room_listAliases_test.go new file mode 100644 index 00000000..71b5002e --- /dev/null +++ b/service/domain/messages/room_listAliases_test.go @@ -0,0 +1,38 @@ +package messages_test + +import ( + "encoding/json" + "testing" + + "github.com/planetary-social/scuttlego/service/domain/messages" + "github.com/planetary-social/scuttlego/service/domain/refs" + "github.com/planetary-social/scuttlego/service/domain/rooms/aliases" + "github.com/planetary-social/scuttlego/service/domain/transport/rpc" + "github.com/stretchr/testify/require" +) + +func TestNewRoomListAliases(t *testing.T) { + identityRef := refs.MustNewIdentity("@Uv38ByGCZU8WP18PmmIdcpVmx00QA3xNe7sEB9Hixkk=.ed25519") + + args, err := messages.NewRoomListAliasesArguments(identityRef) + require.NoError(t, err) + + req, err := messages.NewRoomListAliases(args) + require.NoError(t, err) + require.Equal(t, rpc.ProcedureTypeAsync, req.Type()) + require.Equal(t, rpc.MustNewProcedureName([]string{"room", "listAliases"}), req.Name()) + require.Equal(t, json.RawMessage(`["@Uv38ByGCZU8WP18PmmIdcpVmx00QA3xNe7sEB9Hixkk=.ed25519"]`), req.Arguments()) +} + +func TestNewRoomListAliasesResponseFromBytes(t *testing.T) { + s := `["alias1", "alias2"]` + resp, err := messages.NewRoomListAliasesResponseFromBytes([]byte(s)) + require.NoError(t, err) + require.Equal(t, + []aliases.Alias{ + aliases.MustNewAlias("alias1"), + aliases.MustNewAlias("alias2"), + }, + resp.Aliases(), + ) +} diff --git a/service/domain/messages/room_metadata.go b/service/domain/messages/room_metadata.go new file mode 100644 index 00000000..94a3d5ac --- /dev/null +++ b/service/domain/messages/room_metadata.go @@ -0,0 +1,84 @@ +package messages + +import ( + "encoding/json" + + "github.com/boreq/errors" + "github.com/planetary-social/scuttlego/service/domain/rooms/features" + "github.com/planetary-social/scuttlego/service/domain/transport/rpc" +) + +var ( + RoomMetadataProcedure = rpc.MustNewProcedure( + rpc.MustNewProcedureName([]string{"room", "metadata"}), + rpc.ProcedureTypeAsync, + ) +) + +func NewRoomMetadata() (*rpc.Request, error) { + return rpc.NewRequest( + RoomMetadataProcedure.Name(), + RoomMetadataProcedure.Typ(), + []byte("[]"), + ) +} + +type RoomMetadataResponse struct { + membership bool + features features.Features +} + +func NewRoomMetadataResponseFromBytes(b []byte) (RoomMetadataResponse, error) { + var transport roomMetadataTransport + if err := json.Unmarshal(b, &transport); err != nil { + return RoomMetadataResponse{}, errors.Wrap(err, "json unmarshal failed") + } + + var featuresSlice []features.Feature + + for _, featureString := range transport.Features { + feature, ok := decodeRoomFeature(featureString) + if ok { + featuresSlice = append(featuresSlice, feature) + } + } + + features, err := features.NewFeatures(featuresSlice) + if err != nil { + return RoomMetadataResponse{}, errors.Wrap(err, "could not create features") + } + + return RoomMetadataResponse{ + membership: transport.Membership, + features: features, + }, nil +} + +func NewRoomMetadataResponse(membership bool, ftrs features.Features) RoomMetadataResponse { + return RoomMetadataResponse{ + membership: membership, + features: ftrs, + } +} + +func (r RoomMetadataResponse) Membership() bool { + return r.membership +} + +func (r RoomMetadataResponse) Features() features.Features { + return r.features +} + +type roomMetadataTransport struct { + Membership bool `json:"membership"` + Features []string `json:"features"` +} + +func decodeRoomFeature(s string) (features.Feature, bool) { + switch s { + case "tunnel": + return features.FeatureTunnel, true + default: + return features.Feature{}, false + } +} diff --git a/service/domain/messages/room_metadata_test.go b/service/domain/messages/room_metadata_test.go new file mode 100644 index 00000000..80e71528 --- /dev/null +++ b/service/domain/messages/room_metadata_test.go @@ -0,0 +1,58 @@ +package messages_test + +import ( + "encoding/json" + "testing" + + "github.com/planetary-social/scuttlego/service/domain/messages" + "github.com/planetary-social/scuttlego/service/domain/rooms/features" + "github.com/planetary-social/scuttlego/service/domain/transport/rpc" + "github.com/stretchr/testify/require" +) + +func TestNewRoomMetadata(t *testing.T) { + req, err := messages.NewRoomMetadata() + require.NoError(t, err) + require.Equal(t, rpc.ProcedureTypeAsync, req.Type()) + require.Equal(t, rpc.MustNewProcedureName([]string{"room", "metadata"}), req.Name()) + require.Equal(t, json.RawMessage(`[]`), req.Arguments()) +} + +func TestNewRoomMetadataResponseFromBytes(t *testing.T) { + testCases := []struct { + Name string + String string + ExpectedMembership bool + ExpectedFeatureTunnel bool + }{ + { + Name: "membership_is_true_and_can_tunnel", + String: `{ +"name": "room name", +"membership": true, +"features": ["tunnel", "someunknownfeature"] +}`, + ExpectedMembership: true, + ExpectedFeatureTunnel: true, + }, + { + Name: "membership_is_false_and_can_not_tunnel", + String: `{ +"name": "room name", +"membership": false, +"features": ["someunknownfeature"] +}`, + ExpectedMembership: false, + ExpectedFeatureTunnel: false, + }, + } + + for _, testCase := range testCases { + t.Run(testCase.Name, func(t *testing.T) { + resp, err := messages.NewRoomMetadataResponseFromBytes([]byte(testCase.String)) + require.NoError(t, err) + require.Equal(t, testCase.ExpectedMembership, resp.Membership()) + require.Equal(t, testCase.ExpectedFeatureTunnel, resp.Features().Contains(features.FeatureTunnel)) + }) + } +} diff --git a/service/domain/messages/room_registerAlias.go b/service/domain/messages/room_registerAlias.go index 83be2291..78732e09 100644 --- a/service/domain/messages/room_registerAlias.go +++ b/service/domain/messages/room_registerAlias.go @@ -5,7 +5,7 @@ import ( "encoding/json" "github.com/boreq/errors" - "github.com/planetary-social/scuttlego/service/domain/rooms" + "github.com/planetary-social/scuttlego/service/domain/rooms/aliases" "github.com/planetary-social/scuttlego/service/domain/transport/rpc" ) @@ -30,13 +30,13 @@ func NewRoomRegisterAlias(arguments RoomRegisterAliasArguments) (*rpc.Request, e } type RoomRegisterAliasArguments struct { - alias rooms.Alias - signature rooms.RegistrationSignature + alias aliases.Alias + signature aliases.RegistrationSignature } func NewRoomRegisterAliasArguments( - alias rooms.Alias, - signature rooms.RegistrationSignature, + alias aliases.Alias, + signature aliases.RegistrationSignature, ) (RoomRegisterAliasArguments, error) { if alias.IsZero() { return RoomRegisterAliasArguments{}, errors.New("zero value of alias") @@ -60,17 +60,17 @@ func (i RoomRegisterAliasArguments) MarshalJSON() ([]byte, error) { } type RoomRegisterAliasResponse struct { - url rooms.AliasEndpointURL + url aliases.AliasEndpointURL } func NewRoomRegisterAliasResponseFromBytes(b []byte) (RoomRegisterAliasResponse, error) { - url, err := rooms.NewAliasEndpointURL(string(b)) + url, err := aliases.NewAliasEndpointURL(string(b)) if err != nil { return RoomRegisterAliasResponse{}, errors.Wrap(err, "could not create an endpoint url") } return RoomRegisterAliasResponse{url: url}, nil } -func (r RoomRegisterAliasResponse) AliasEndpointURL() rooms.AliasEndpointURL { +func (r RoomRegisterAliasResponse) AliasEndpointURL() aliases.AliasEndpointURL { return r.url } diff --git a/service/domain/messages/room_registerAlias_test.go b/service/domain/messages/room_registerAlias_test.go new file mode 100644 index 00000000..97df4017 --- /dev/null +++ b/service/domain/messages/room_registerAlias_test.go @@ -0,0 +1,55 @@ +package messages_test + +import ( + "encoding/base64" + "encoding/json" + "testing" + + "github.com/planetary-social/scuttlego/fixtures" + "github.com/planetary-social/scuttlego/service/domain/messages" + "github.com/planetary-social/scuttlego/service/domain/refs" + "github.com/planetary-social/scuttlego/service/domain/rooms/aliases" + "github.com/planetary-social/scuttlego/service/domain/transport/rpc" + "github.com/stretchr/testify/require" +) + +func TestNewRoomRegisterAlias(t *testing.T) { + alias := aliases.MustNewAlias("somealias") + userIdentity := fixtures.SomePrivateIdentity() + userRef := refs.MustNewIdentityFromPublic(userIdentity.Public()) + roomRef := fixtures.SomeRefIdentity() + + message, err := aliases.NewRegistrationMessage(alias, userRef, roomRef) + require.NoError(t, err) + + signature, err := aliases.NewRegistrationSignature(message, userIdentity) + require.NoError(t, err) + + args, err := messages.NewRoomRegisterAliasArguments(alias, signature) + require.NoError(t, err) + + req, err := messages.NewRoomRegisterAlias(args) + require.NoError(t, err) + require.Equal(t, rpc.ProcedureTypeAsync, req.Type()) + require.Equal(t, rpc.MustNewProcedureName([]string{"room", "registerAlias"}), req.Name()) + + var actualArgs []string + err = json.Unmarshal(req.Arguments(), &actualArgs) + require.NoError(t, err) + require.Equal(t, + []string{ + alias.String(), + base64.StdEncoding.EncodeToString(signature.Bytes()) + ".sig.ed25519", + }, + actualArgs, + ) +} + +func TestNewRoomRegisterAliasResponseFromBytes(t *testing.T) { + resp, err := messages.NewRoomRegisterAliasResponseFromBytes([]byte("somealiasurl")) + require.NoError(t, err) + require.Equal(t, + aliases.MustNewAliasEndpointURL("somealiasurl"), + resp.AliasEndpointURL(), + ) +} diff --git a/service/domain/messages/room_revokeAlias.go b/service/domain/messages/room_revokeAlias.go index 09b964cc..e3fe5c40 100644 --- a/service/domain/messages/room_revokeAlias.go +++ b/service/domain/messages/room_revokeAlias.go @@ -4,7 +4,7 @@ import ( "encoding/json" "github.com/boreq/errors" - "github.com/planetary-social/scuttlego/service/domain/rooms" + "github.com/planetary-social/scuttlego/service/domain/rooms/aliases" "github.com/planetary-social/scuttlego/service/domain/transport/rpc" ) @@ -29,11 +29,11 @@ func NewRoomRevokeAlias(arguments RoomRevokeAliasArguments) (*rpc.Request, error } type RoomRevokeAliasArguments struct { - alias rooms.Alias + alias aliases.Alias } func NewRoomRevokeAliasArguments( - alias rooms.Alias, + alias aliases.Alias, ) (RoomRevokeAliasArguments, error) { if alias.IsZero() { return RoomRevokeAliasArguments{}, errors.New("zero value of alias") diff --git a/service/domain/messages/room_revokeAlias_test.go b/service/domain/messages/room_revokeAlias_test.go new file mode 100644 index 00000000..d31c4487 --- /dev/null +++ b/service/domain/messages/room_revokeAlias_test.go @@ -0,0 +1,24 @@ +package messages_test + +import ( + "encoding/json" + "testing" + + "github.com/planetary-social/scuttlego/service/domain/messages" + "github.com/planetary-social/scuttlego/service/domain/rooms/aliases" + "github.com/planetary-social/scuttlego/service/domain/transport/rpc" + "github.com/stretchr/testify/require" +) + +func TestNewRoomRevokeAlias(t *testing.T) { + alias := aliases.MustNewAlias("somealias") + + args, err := messages.NewRoomRevokeAliasArguments(alias) + require.NoError(t, err) + + req, err := messages.NewRoomRevokeAlias(args) + require.NoError(t, err) + require.Equal(t, rpc.ProcedureTypeAsync, req.Type()) + require.Equal(t, rpc.MustNewProcedureName([]string{"room", "revokeAlias"}), req.Name()) + require.Equal(t, json.RawMessage(`["somealias"]`), req.Arguments()) +} diff --git a/service/domain/messages/tunnel_connect.go b/service/domain/messages/tunnel_connect.go new file mode 100644 index 00000000..7e264f49 --- /dev/null +++ b/service/domain/messages/tunnel_connect.go @@ -0,0 +1,66 @@ +package messages + +import ( + "encoding/json" + + "github.com/boreq/errors" + "github.com/planetary-social/scuttlego/service/domain/refs" + "github.com/planetary-social/scuttlego/service/domain/transport/rpc" +) + +var ( + TunnelConnectProcedure = rpc.MustNewProcedure( + rpc.MustNewProcedureName([]string{"tunnel", "connect"}), + rpc.ProcedureTypeDuplex, + ) +) + +func NewTunnelConnectToPortal(arguments TunnelConnectToPortalArguments) (*rpc.Request, error) { + j, err := arguments.MarshalJSON() + if err != nil { + return nil, errors.Wrap(err, "failed to marshal arguments") + } + + return rpc.NewRequest( + TunnelConnectProcedure.Name(), + TunnelConnectProcedure.Typ(), + j, + ) +} + +type TunnelConnectToPortalArguments struct { + portal refs.Identity + target refs.Identity +} + +func NewTunnelConnectToPortalArguments( + portal refs.Identity, + target refs.Identity, +) (TunnelConnectToPortalArguments, error) { + if portal.IsZero() { + return TunnelConnectToPortalArguments{}, errors.New("zero value of portal identity") + } + + if target.IsZero() { + return TunnelConnectToPortalArguments{}, errors.New("zero value of target identity") + } + + return TunnelConnectToPortalArguments{ + portal: portal, + target: target, + }, nil +} + +func (i TunnelConnectToPortalArguments) MarshalJSON() ([]byte, error) { + return json.Marshal([]tunnelConnectToPortalArgumentsTransport{ + { + Portal: i.portal.String(), + Target: i.target.String(), + }, + }) +} + +type tunnelConnectToPortalArgumentsTransport struct { + Portal string `json:"portal"` + Target string `json:"target"` +} diff --git a/service/domain/messages/tunnel_connect_test.go b/service/domain/messages/tunnel_connect_test.go new file mode 100644 index 00000000..34221ceb --- /dev/null +++ b/service/domain/messages/tunnel_connect_test.go @@ -0,0 +1,28 @@ +package messages_test + +import ( + "encoding/json" + "testing" + + "github.com/planetary-social/scuttlego/service/domain/messages" + "github.com/planetary-social/scuttlego/service/domain/refs" + "github.com/planetary-social/scuttlego/service/domain/transport/rpc" + "github.com/stretchr/testify/require" +) + +func TestNewTunnelConnectToPortal(t *testing.T) { + portalRef := refs.MustNewIdentity("@gYVa2GgdDYbR6R4AFnk5y2aU0sQirNIIoAcpOUh/aZk=.ed25519") + targetRef := refs.MustNewIdentity("@Uv38ByGCZU8WP18PmmIdcpVmx00QA3xNe7sEB9Hixkk=.ed25519") + + args, err := messages.NewTunnelConnectToPortalArguments(portalRef, targetRef) + require.NoError(t, err) + + req, err := messages.NewTunnelConnectToPortal(args) + require.NoError(t, err) + require.Equal(t, rpc.ProcedureTypeDuplex, req.Type()) + require.Equal(t, rpc.MustNewProcedureName([]string{"tunnel", "connect"}), req.Name()) + require.Equal(t, + json.RawMessage(`[{"portal":"@gYVa2GgdDYbR6R4AFnk5y2aU0sQirNIIoAcpOUh/aZk=.ed25519","target":"@Uv38ByGCZU8WP18PmmIdcpVmx00QA3xNe7sEB9Hixkk=.ed25519"}]`), + req.Arguments(), + ) +} diff --git a/service/domain/transport/mocks/connection.go b/service/domain/mocks/connection.go similarity index 100% rename from service/domain/transport/mocks/connection.go rename to service/domain/mocks/connection.go diff --git a/service/domain/mocks/peer_manager.go b/service/domain/mocks/peer_manager.go new file mode 100644 index 00000000..6b5b37b6 --- /dev/null +++ b/service/domain/mocks/peer_manager.go @@ -0,0 +1,57 @@ +package mocks + +import ( + "github.com/boreq/errors" + "github.com/planetary-social/scuttlego/service/domain/identity" + "github.com/planetary-social/scuttlego/service/domain/network" + "github.com/planetary-social/scuttlego/service/domain/transport" +) + +type PeerManagerMock struct { + connectViaRoomCalls []PeerManagerConnectViaRoomCall + peersReturnValue []transport.Peer +} + +func NewPeerManagerMock() *PeerManagerMock { + return &PeerManagerMock{} +} + +func (p *PeerManagerMock) Connect(remote identity.Public, address network.Address) error { + return errors.New("not implemented") +} + +func (p *PeerManagerMock) ConnectViaRoom(portal transport.Peer, target identity.Public) error { + p.connectViaRoomCalls = append( + p.connectViaRoomCalls, + PeerManagerConnectViaRoomCall{ + Portal: portal, + Target: target, + }, + ) + return nil +} + +func (p *PeerManagerMock) ConnectViaRoomCalls() []PeerManagerConnectViaRoomCall { + return p.connectViaRoomCalls +} + +func (p *PeerManagerMock) EstablishNewConnections() error { + return errors.New("not implemented") +} + +func (p *PeerManagerMock) ProcessNewLocalDiscovery(remote identity.Public, address network.Address) error { + return errors.New("not implemented") +} + +func (p *PeerManagerMock) Peers() []transport.Peer { + return p.peersReturnValue +} + +func (p *PeerManagerMock) MockPeers(peers []transport.Peer) { + p.peersReturnValue = peers +} + +type PeerManagerConnectViaRoomCall struct { + Portal transport.Peer + Target identity.Public +} diff --git a/service/domain/network/dialer.go b/service/domain/network/dialer.go index b0b416fd..27d72bbb 100644 --- a/service/domain/network/dialer.go +++ b/service/domain/network/dialer.go @@ -4,6 +4,7 @@ import ( "context" "io" "net" + "time" "github.com/boreq/errors" "github.com/planetary-social/scuttlego/logging" @@ -11,6 +12,10 @@ import ( "github.com/planetary-social/scuttlego/service/domain/transport" ) +const ( + dialTimeout = 15 * time.Second +) + type ClientPeerInitializer interface { // InitializeClientPeer initializes outgoing connections by performing a handshake and establishing an RPC // connection using the provided ReadWriteCloser. Context is used as the RPC connection context. @@ -30,7 +35,11 @@ func NewDialer(initializer ClientPeerInitializer, logger logging.Logger) (*Diale } func (d Dialer) DialWithInitializer(ctx context.Context, initializer ClientPeerInitializer, remote identity.Public, addr Address) (transport.Peer, error) { - conn, err := net.Dial("tcp", addr.String()) + dialer := net.Dialer{ + Timeout: dialTimeout, + } + + conn, err := dialer.Dial("tcp", addr.String()) if err != nil { return transport.Peer{}, errors.Wrap(err, "could not dial") } diff --git a/service/domain/peer_manager.go b/service/domain/peer_manager.go index 7d015ad2..6f2eb924 100644 --- a/service/domain/peer_manager.go +++ b/service/domain/peer_manager.go @@ -18,6 +18,10 @@ type Dialer interface { Dial(ctx context.Context, remote identity.Public, address network.Address) (transport.Peer, error) } +type RoomDialer interface { + DialViaRoom(ctx context.Context, portal transport.Peer, target identity.Public) (transport.Peer, error) +} + type MessageReplicator interface { Replicate(ctx context.Context, peer transport.Peer) error } @@ -26,6 +30,10 @@ type BlobReplicator interface { Replicate(ctx context.Context, peer transport.Peer) error } +type RoomScanner interface { + Run(ctx context.Context, peer transport.Peer) error +} + type PeerManagerConfig struct { // Peer manager will attempt to remain connected to the preferred pubs. PreferredPubs []Pub @@ -45,8 +53,10 @@ type PeerManager struct { config PeerManagerConfig dialer Dialer + roomDialer RoomDialer messageReplicator MessageReplicator blobReplicator BlobReplicator + roomScanner RoomScanner logger logging.Logger } @@ -55,9 +65,11 @@ type PeerManager struct { func NewPeerManager( ctx context.Context, config PeerManagerConfig, + dialer Dialer, + roomDialer RoomDialer, messageReplicator MessageReplicator, blobReplicator BlobReplicator, - dialer Dialer, + roomScanner RoomScanner, logger logging.Logger, ) *PeerManager { return &PeerManager{ @@ -66,8 +78,10 @@ func NewPeerManager( peersLock: &sync.Mutex{}, config: config, dialer: dialer, + roomDialer: roomDialer, messageReplicator: messageReplicator, blobReplicator: blobReplicator, + roomScanner: roomScanner, logger: logger.New("peer_manager"), } } @@ -102,10 +116,12 @@ func (p PeerManager) Peers() []transport.Peer { return result } -// Connect attempts to establish communications with the specified peer. If a connection to the specified peer -// already exists then a new connection will not be initiated. If connecting to the peer succeeds but in the meantime -// a connection to the same node was created manually or automatically by the manager then the old connection will be -// replaced by the new connection and terminated. +// Connect attempts to establish communications with the specified peer. If a +// connection to the specified peer already exists then a new connection will +// not be initiated. If connecting to the peer succeeds but in the meantime a +// connection to the same node was created manually or automatically by the +// manager then the old connection will be replaced by the new connection and +// terminated. func (p PeerManager) Connect(remote identity.Public, address network.Address) error { select { case <-p.ctx.Done(): @@ -129,6 +145,31 @@ func (p PeerManager) Connect(remote identity.Public, address network.Address) er return nil } +// ConnectViaRoom attempts to establish communications with the specified peer +// using a room as a relay. Behaves like Connect. +func (p PeerManager) ConnectViaRoom(portal transport.Peer, target identity.Public) error { + select { + case <-p.ctx.Done(): + return errors.Wrap(p.ctx.Err(), "context is done so the connection would just terminate right away") + default: + } + + if p.alreadyConnected(target) { // early check + return nil + } + + p.logger.WithField("target", target).WithField("portal", portal).Debug("dialing via room") + + peer, err := p.roomDialer.DialViaRoom(p.ctx, portal, target) + if err != nil { + return errors.Wrap(err, "dial via room failed") + } + + p.HandleNewPeer(peer) + + return nil +} + // ProcessNewLocalDiscovery handles incoming local peer announcements. func (p PeerManager) ProcessNewLocalDiscovery(remote identity.Public, address network.Address) error { return p.Connect(remote, address) @@ -190,35 +231,22 @@ func (p PeerManager) peerKey(remote identity.Public) string { // todo this probably shouldn't be handled by the peer manager func (p PeerManager) processConnection(peer transport.Peer) { p.logger.WithField("peer", peer).Debug("handling a new peer") - if err := p.handleConnection(peer); err != nil { - p.logger.WithError(err).WithField("peer", peer).Debug("connection ended") + if err := p.runTasks(peer); err != nil { + p.logger.WithError(err).WithField("peer", peer).Debug("all tasks ended") } } -func (p PeerManager) handleConnection(peer transport.Peer) error { +func (p PeerManager) runTasks(peer transport.Peer) error { ch := make(chan error) ctx, cancel := context.WithCancel(peer.Conn().Context()) + defer cancel() tasks := 0 - tasks++ - go func() { - defer cancel() - defer p.logger.Debug("message replication task terminating") - if err := p.messageReplicator.Replicate(ctx, peer); err != nil { - ch <- err - } - }() - - tasks++ - go func() { - defer cancel() - defer p.logger.Debug("blob replication task terminating") - if err := p.blobReplicator.Replicate(ctx, peer); err != nil { - ch <- err - } - }() + p.startTask(&tasks, ctx, peer, ch, p.messageReplicator.Replicate, "message replication") + p.startTask(&tasks, ctx, peer, ch, p.blobReplicator.Replicate, "blob replication") + p.startTask(&tasks, ctx, peer, ch, p.roomScanner.Run, "room scanner") var result error for i := 0; i < tasks; i++ { @@ -227,6 +255,23 @@ func (p PeerManager) handleConnection(peer transport.Peer) error { return result } +func (p PeerManager) startTask( + tasks *int, + ctx context.Context, + peer transport.Peer, + ch chan<- error, + fn func(ctx context.Context, peer transport.Peer) error, + taskName string, +) { + peerLogger := p.logger.WithField("peer", peer) + *tasks = *tasks + 1 + go func() { + err := fn(ctx, peer) + peerLogger.WithError(err).WithField("task", taskName).Debug("task terminating") + ch <- err + }() +} + type connectedPeer struct { peer transport.Peer added time.Time diff --git a/service/domain/peer_manager_test.go b/service/domain/peer_manager_test.go index c39833c8..61410a03 100644 --- a/service/domain/peer_manager_test.go +++ b/service/domain/peer_manager_test.go @@ -21,7 +21,7 @@ import ( func TestPeerManager_PeersAreTracked(t *testing.T) { m := buildTestPeerManager(t) - peer1 := transport.NewPeer(fixtures.SomePublicIdentity(), newConnectionMock()) + peer1 := transport.MustNewPeer(fixtures.SomePublicIdentity(), newConnectionMock()) m.Manager.HandleNewPeer(peer1) require.Equal(t, @@ -31,7 +31,7 @@ func TestPeerManager_PeersAreTracked(t *testing.T) { m.Manager.Peers(), ) - peer2 := transport.NewPeer(fixtures.SomePublicIdentity(), newConnectionMock()) + peer2 := transport.MustNewPeer(fixtures.SomePublicIdentity(), newConnectionMock()) m.Manager.HandleNewPeer(peer2) expectedPeers := []transport.Peer{ @@ -54,7 +54,7 @@ func TestPeerManager_IfConnectionClosesThenPeerIsRemovedFromManager(t *testing.T m := buildTestPeerManager(t) conn := newConnectionMock() - peer := transport.NewPeer(fixtures.SomePublicIdentity(), conn) + peer := transport.MustNewPeer(fixtures.SomePublicIdentity(), conn) m.Manager.HandleNewPeer(peer) require.Equal(t, @@ -81,7 +81,7 @@ func TestPeerManager_OnlyLatestConnectionToIdentityIsKept(t *testing.T) { iden := fixtures.SomePublicIdentity() conn1 := newConnectionMock() - peer1 := transport.NewPeer(iden, conn1) + peer1 := transport.MustNewPeer(iden, conn1) m.Manager.HandleNewPeer(peer1) require.Equal(t, @@ -92,7 +92,7 @@ func TestPeerManager_OnlyLatestConnectionToIdentityIsKept(t *testing.T) { ) conn2 := newConnectionMock() - peer2 := transport.NewPeer(iden, conn2) + peer2 := transport.MustNewPeer(iden, conn2) m.Manager.HandleNewPeer(peer2) require.Equal(t, @@ -114,7 +114,7 @@ func TestPeerManager_Connect_DialsAnIdentityIfNotConnectedToIt(t *testing.T) { m := buildTestPeerManager(t) address := network.NewAddress("some address") - peer := transport.NewPeer(fixtures.SomePublicIdentity(), newConnectionMock()) + peer := transport.MustNewPeer(fixtures.SomePublicIdentity(), newConnectionMock()) m.Dialer.AddPeer(peer, address) require.Empty(t, m.Manager.Peers()) @@ -141,7 +141,7 @@ func TestPeerManager_Connect_DoesNotDialIfAlreadyConnectedToIdentity(t *testing. m := buildTestPeerManager(t) iden := fixtures.SomePublicIdentity() - alreadyConnectedPeer := transport.NewPeer(iden, newConnectionMock()) + alreadyConnectedPeer := transport.MustNewPeer(iden, newConnectionMock()) m.Manager.HandleNewPeer(alreadyConnectedPeer) err := m.Manager.Connect(iden, network.NewAddress("someAddress")) @@ -164,7 +164,7 @@ func TestPeerManager_EstablishNewConnections_ConnectsToPreferredPubs(t *testing. m := buildTestPeerManagerWithConfig(t, config) - peer := transport.NewPeer(pub.Identity, newConnectionMock()) + peer := transport.MustNewPeer(pub.Identity, newConnectionMock()) m.Dialer.AddPeer(peer, pub.Address) require.Empty(t, m.Manager.Peers()) @@ -201,7 +201,7 @@ func TestPeerManager_EstablishNewConnections_DoesNotConnectToPreferredPubsIfAlre m := buildTestPeerManagerWithConfig(t, config) - peer := transport.NewPeer(pub.Identity, newConnectionMock()) + peer := transport.MustNewPeer(pub.Identity, newConnectionMock()) m.Dialer.AddPeer(peer, pub.Address) err := m.Manager.EstablishNewConnections() @@ -217,7 +217,7 @@ func TestPeerManager_ProcessNewLocalDiscovery_ConnectsToPeerOnDiscovery(t *testi m := buildTestPeerManager(t) address := network.NewAddress("some address") - peer := transport.NewPeer(fixtures.SomePublicIdentity(), newConnectionMock()) + peer := transport.MustNewPeer(fixtures.SomePublicIdentity(), newConnectionMock()) m.Dialer.AddPeer(peer, address) require.Empty(t, m.Manager.Peers()) @@ -244,7 +244,7 @@ func TestPeerManager_ProcessNewLocalDiscovery_DoesNotConnectIfAlreadyConnected(t m := buildTestPeerManager(t) iden := fixtures.SomePublicIdentity() - alreadyConnectedPeer := transport.NewPeer(iden, newConnectionMock()) + alreadyConnectedPeer := transport.MustNewPeer(iden, newConnectionMock()) m.Manager.HandleNewPeer(alreadyConnectedPeer) err := m.Manager.ProcessNewLocalDiscovery(iden, network.NewAddress("someAddress")) @@ -262,11 +262,13 @@ func buildTestPeerManagerWithConfig(t *testing.T, config domain.PeerManagerConfi ctx := fixtures.TestContext(t) logger := logging.NewDevNullLogger() + dialer := newDialerMock() + roomDialer := newRoomDialerMock() msgReplicator := newMessageReplicatorMock() blobReplicator := newBlobReplicatorMock() - dialer := newDialerMock() + roomScanner := newRoomScannerMock() - manager := domain.NewPeerManager(ctx, config, msgReplicator, blobReplicator, dialer, logger) + manager := domain.NewPeerManager(ctx, config, dialer, roomDialer, msgReplicator, blobReplicator, roomScanner, logger) return testPeerManager{ Manager: manager, @@ -331,6 +333,17 @@ func (d *dialerMock) Dial(ctx context.Context, remote identity.Public, address n return peer, nil } +type roomDialerMock struct { +} + +func newRoomDialerMock() *roomDialerMock { + return &roomDialerMock{} +} + +func (r roomDialerMock) DialViaRoom(ctx context.Context, portal transport.Peer, target identity.Public) (transport.Peer, error) { + return transport.Peer{}, errors.New("not implemented") +} + type connectionMock struct { ctx context.Context cancel context.CancelFunc @@ -370,6 +383,17 @@ func (c *connectionMock) IsClosed() bool { } } +type roomScannerMock struct { +} + +func newRoomScannerMock() *roomScannerMock { + return &roomScannerMock{} +} + +func (r roomScannerMock) Run(ctx context.Context, peer transport.Peer) error { + return errors.New("not implemented") +} + func eventually(t *testing.T, condition func() bool, msgAndArgs ...any) { require.Eventually(t, condition, 1*time.Second, 10*time.Millisecond, msgAndArgs) } diff --git a/service/domain/replication/ebt/replicator_test.go b/service/domain/replication/ebt/replicator_test.go index c215c038..18552b5f 100644 --- a/service/domain/replication/ebt/replicator_test.go +++ b/service/domain/replication/ebt/replicator_test.go @@ -21,7 +21,7 @@ func TestReplicator_ReplicateCallsWaitForSessionIfConnectionWasInitiatedByRemote ctx := fixtures.TestContext(t) ctx = rpc.PutConnectionIdInContext(ctx, connectionId) connThatWasInitiatedByRemote := newConnectionMock(true) - peer := transport.NewPeer(fixtures.SomePublicIdentity(), connThatWasInitiatedByRemote) + peer := transport.MustNewPeer(fixtures.SomePublicIdentity(), connThatWasInitiatedByRemote) tr.Tracker.WaitForSessionResult = true @@ -42,7 +42,7 @@ func TestReplicator_ReplicateInitiatesTheSessionIfConnectionWasNotInitiatedByRem ctx := fixtures.TestContext(t) ctx = rpc.PutConnectionIdInContext(ctx, connectionId) connectionThatWasNotInitiatedByRemote := newConnectionMock(false) - peer := transport.NewPeer(fixtures.SomePublicIdentity(), connectionThatWasNotInitiatedByRemote) + peer := transport.MustNewPeer(fixtures.SomePublicIdentity(), connectionThatWasNotInitiatedByRemote) err := tr.Replicator.Replicate(ctx, peer) require.NoError(t, err) @@ -66,7 +66,7 @@ func TestReplicator_ReplicateReturnsAnErrorAndDoesNotWaitIfOpenSessionReturnsAnE ctx := fixtures.TestContext(t) ctx = rpc.PutConnectionIdInContext(ctx, connectionId) conn := newConnectionMock(false) - peer := transport.NewPeer(fixtures.SomePublicIdentity(), conn) + peer := transport.MustNewPeer(fixtures.SomePublicIdentity(), conn) tr.Tracker.OpenSessionError = fixtures.SomeError() @@ -87,7 +87,7 @@ func TestReplicator_ReplicateReturnsErrPeerDoesNotSupportEbtIfRemoteNeverOpensAS ctx := fixtures.TestContext(t) ctx = rpc.PutConnectionIdInContext(ctx, connectionId) conn := newConnectionMock(true) - peer := transport.NewPeer(fixtures.SomePublicIdentity(), conn) + peer := transport.MustNewPeer(fixtures.SomePublicIdentity(), conn) tr.Tracker.WaitForSessionResult = false diff --git a/service/domain/replication/negotiator.go b/service/domain/replication/negotiator.go index ea2b15c9..7863d5ff 100644 --- a/service/domain/replication/negotiator.go +++ b/service/domain/replication/negotiator.go @@ -37,7 +37,7 @@ func NewNegotiator( chsReplicator CreateHistoryStreamReplicator, ) *Negotiator { return &Negotiator{ - logger: logger, + logger: logger.New("replication_negotiator"), ebtReplicator: ebtReplicator, chsReplicator: chsReplicator, } diff --git a/service/domain/replication/negotiator_test.go b/service/domain/replication/negotiator_test.go index fa1fd431..8b5050db 100644 --- a/service/domain/replication/negotiator_test.go +++ b/service/domain/replication/negotiator_test.go @@ -5,6 +5,7 @@ import ( "testing" "github.com/planetary-social/scuttlego/fixtures" + "github.com/planetary-social/scuttlego/service/domain/mocks" "github.com/planetary-social/scuttlego/service/domain/replication" "github.com/planetary-social/scuttlego/service/domain/transport" "github.com/stretchr/testify/require" @@ -66,7 +67,8 @@ func TestNegotiator(t *testing.T) { chsReplicator.ReturnError = testCase.ChsError ctx := fixtures.TestContext(t) - peer := transport.NewPeer(fixtures.SomePublicIdentity(), nil) + conn := mocks.NewConnectionMock(ctx) + peer := transport.MustNewPeer(fixtures.SomePublicIdentity(), conn) err := negotiator.Replicate(ctx, peer) if testCase.ExpectedError != nil { diff --git a/service/domain/rooms/rooms.go b/service/domain/rooms/aliases/aliases.go similarity index 95% rename from service/domain/rooms/rooms.go rename to service/domain/rooms/aliases/aliases.go index f4a17c22..cb3a5428 100644 --- a/service/domain/rooms/rooms.go +++ b/service/domain/rooms/aliases/aliases.go @@ -1,4 +1,4 @@ -package rooms +package aliases import ( "crypto/ed25519" @@ -148,6 +148,14 @@ func NewAliasEndpointURL(s string) (AliasEndpointURL, error) { return AliasEndpointURL{s: s}, nil } +func MustNewAliasEndpointURL(s string) AliasEndpointURL { + v, err := NewAliasEndpointURL(s) + if err != nil { + panic(err) + } + return v +} + func (a AliasEndpointURL) String() string { return a.s } diff --git a/service/domain/rooms/rooms_test.go b/service/domain/rooms/aliases/aliases_test.go similarity index 82% rename from service/domain/rooms/rooms_test.go rename to service/domain/rooms/aliases/aliases_test.go index b2ba5ee6..e0f771df 100644 --- a/service/domain/rooms/rooms_test.go +++ b/service/domain/rooms/aliases/aliases_test.go @@ -1,4 +1,4 @@ -package rooms_test +package aliases_test import ( "strings" @@ -7,7 +7,7 @@ import ( "github.com/boreq/errors" "github.com/planetary-social/scuttlego/fixtures" "github.com/planetary-social/scuttlego/service/domain/refs" - "github.com/planetary-social/scuttlego/service/domain/rooms" + "github.com/planetary-social/scuttlego/service/domain/rooms/aliases" "github.com/stretchr/testify/require" ) @@ -36,7 +36,7 @@ func TestNewAlias(t *testing.T) { for _, testCase := range testCases { t.Run(testCase.Alias, func(t *testing.T) { - alias, err := rooms.NewAlias(testCase.Alias) + alias, err := aliases.NewAlias(testCase.Alias) if testCase.ExpectedError == nil { require.NoError(t, err) require.Equal(t, testCase.Alias, alias.String()) @@ -49,7 +49,7 @@ func TestNewAlias(t *testing.T) { } func TestRegistrationMessage_ProducesExpectedMessageString(t *testing.T) { - alias, err := rooms.NewAlias("somealias") + alias, err := aliases.NewAlias("somealias") require.NoError(t, err) user, err := refs.NewIdentity("@gYVa2GgdDYbR6R4AFnk5y2aU0sQirNIIoAcpOUh/aZk=.ed25519") @@ -58,7 +58,7 @@ func TestRegistrationMessage_ProducesExpectedMessageString(t *testing.T) { room, err := refs.NewIdentity("@Uv38ByGCZU8WP18PmmIdcpVmx00QA3xNe7sEB9Hixkk=.ed25519") require.NoError(t, err) - message, err := rooms.NewRegistrationMessage(alias, user, room) + message, err := aliases.NewRegistrationMessage(alias, user, room) require.NoError(t, err) require.Equal(t, @@ -70,24 +70,24 @@ func TestRegistrationMessage_ProducesExpectedMessageString(t *testing.T) { func TestNewRegistrationSignature_PrivateIdentityMustMatchUserFromTheMessage(t *testing.T) { identity := fixtures.SomePrivateIdentity() - message, err := rooms.NewRegistrationMessage(fixtures.SomeAlias(), fixtures.SomeRefIdentity(), fixtures.SomeRefIdentity()) + message, err := aliases.NewRegistrationMessage(fixtures.SomeAlias(), fixtures.SomeRefIdentity(), fixtures.SomeRefIdentity()) require.NoError(t, err) - _, err = rooms.NewRegistrationSignature(message, identity) + _, err = aliases.NewRegistrationSignature(message, identity) require.EqualError(t, err, "private identity doesn't match user identity from the message") } func TestRegistrationSignature_CreatesNonZeroSignatures(t *testing.T) { identity := fixtures.SomePrivateIdentity() - message, err := rooms.NewRegistrationMessage( + message, err := aliases.NewRegistrationMessage( fixtures.SomeAlias(), refs.MustNewIdentityFromPublic(identity.Public()), fixtures.SomeRefIdentity(), ) require.NoError(t, err) - signature, err := rooms.NewRegistrationSignature(message, identity) + signature, err := aliases.NewRegistrationSignature(message, identity) require.NoError(t, err) require.NotEmpty(t, signature.Bytes()) require.False(t, signature.IsZero()) @@ -118,7 +118,7 @@ func TestNewAliasEndpointURL(t *testing.T) { for _, testCase := range testCases { t.Run(testCase.String, func(t *testing.T) { - url, err := rooms.NewAliasEndpointURL(testCase.String) + url, err := aliases.NewAliasEndpointURL(testCase.String) if testCase.ExpectedError == nil { require.NoError(t, err) require.Equal(t, testCase.String, url.String()) diff --git a/service/domain/rooms/features/features.go b/service/domain/rooms/features/features.go new file mode 100644 index 00000000..21896202 --- /dev/null +++ b/service/domain/rooms/features/features.go @@ -0,0 +1,56 @@ +package features + +import ( + "fmt" + + "github.com/boreq/errors" + "github.com/planetary-social/scuttlego/internal" +) + +type Features struct { + features internal.Set[Feature] +} + +func NewFeatures(features []Feature) (Features, error) { + featuresSet := internal.NewSet[Feature]() + + for _, feature := range features { + if feature.IsZero() { + return Features{}, errors.New("zero value of feature") + } + + if featuresSet.Contains(feature) { + return Features{}, fmt.Errorf("duplicate feature: %+v", feature.s) + } + + featuresSet.Put(feature) + } + + return Features{ + features: featuresSet, + }, nil +} + +func MustNewFeatures(features []Feature) Features { + v, err := NewFeatures(features) + if err != nil { + panic(err) + } + return v +} + +func (f Features) Contains(feature Feature) bool { + return f.features.Contains(feature) +} + +var ( + FeatureTunnel = Feature{"tunnel"} +) + +type Feature struct { + s string +} + +func (f Feature) IsZero() bool { + return f == Feature{} +} diff --git a/service/domain/rooms/features/features_test.go b/service/domain/rooms/features/features_test.go new file mode 100644 index 00000000..3c3e3f19 --- /dev/null +++ b/service/domain/rooms/features/features_test.go @@ -0,0 +1,52 @@ +package features_test + +import ( + "testing" + + "github.com/boreq/errors" + "github.com/planetary-social/scuttlego/service/domain/rooms/features" + "github.com/stretchr/testify/require" +) + +func TestNewFeatures(t *testing.T) { + testCases := []struct { + Name string + Features []features.Feature + ExpectedError error + }{ + { + Name: "zero_value_of_feature", + Features: []features.Feature{{}}, + ExpectedError: errors.New("zero value of feature"), + }, + { + Name: "empty_slice", + Features: nil, + ExpectedError: nil, + }, + { + Name: "one_feature", + Features: []features.Feature{features.FeatureTunnel}, + ExpectedError: nil, + }, + { + Name: "duplicated_features", + Features: []features.Feature{features.FeatureTunnel, features.FeatureTunnel}, + ExpectedError: errors.New("duplicate feature: tunnel"), + }, + } + + for _, testCase := range testCases { + t.Run(testCase.Name, func(t *testing.T) { + features, err := features.NewFeatures(testCase.Features) + if testCase.ExpectedError != nil { + require.EqualError(t, err, testCase.ExpectedError.Error()) + } else { + require.NoError(t, err) + for _, feature := range testCase.Features { + require.True(t, features.Contains(feature)) + } + } + }) + } +} diff --git a/service/domain/rooms/rpc.go b/service/domain/rooms/rpc.go new file mode 100644 index 00000000..91ba2f6b --- /dev/null +++ b/service/domain/rooms/rpc.go @@ -0,0 +1,223 @@ +package rooms + +import ( + "context" + + "github.com/boreq/errors" + "github.com/planetary-social/scuttlego/logging" + "github.com/planetary-social/scuttlego/service/domain/messages" + "github.com/planetary-social/scuttlego/service/domain/refs" + "github.com/planetary-social/scuttlego/service/domain/transport" + "github.com/planetary-social/scuttlego/service/domain/transport/rpc" +) + +type PeerRPCAdapter struct { + logger logging.Logger +} + +func NewPeerRPCAdapter(logger logging.Logger) *PeerRPCAdapter { + return &PeerRPCAdapter{ + logger: logger.New("rooms_peer_rpc_adapter"), + } +} + +func (a *PeerRPCAdapter) GetMetadata(ctx context.Context, peer transport.Peer) (messages.RoomMetadataResponse, error) { + ctx, cancel := context.WithCancel(ctx) + defer cancel() + + req, err := messages.NewRoomMetadata() + if err != nil { + return messages.RoomMetadataResponse{}, errors.Wrap(err, "could not create a request") + } + + stream, err := peer.Conn().PerformRequest(ctx, req) + if err != nil { + return messages.RoomMetadataResponse{}, errors.Wrap(err, "could not perform a request") + } + + v, ok := <-stream.Channel() + if !ok { + return messages.RoomMetadataResponse{}, errors.New("received no responses") + } + + if err := v.Err; err != nil { + return messages.RoomMetadataResponse{}, errors.Wrap(err, "received an error") + } + + metadataResponse, err := messages.NewRoomMetadataResponseFromBytes(v.Value.Bytes()) + if err != nil { + return messages.RoomMetadataResponse{}, errors.Wrap(err, "could not parse the response") + } + + return metadataResponse, nil +} + +func (a *PeerRPCAdapter) GetAttendants(ctx context.Context, peer transport.Peer) (<-chan RoomAttendantsEvent, error) { + req, err := messages.NewRoomAttendants() + if err != nil { + return nil, errors.Wrap(err, "could not create a request") + } + + stream, err := peer.Conn().PerformRequest(ctx, req) + if err != nil { + return nil, errors.Wrap(err, "could not perform a request") + } + + ch := make(chan RoomAttendantsEvent) + + go func() { + defer close(ch) + + if err := a.streamAttendants(ctx, ch, stream); err != nil { + a.logger.WithError(err).Debug("attendants stream error") + } + }() + + return ch, nil +} + +func (a *PeerRPCAdapter) streamAttendants(ctx context.Context, ch chan RoomAttendantsEvent, stream rpc.ResponseStream) error { + v, ok := <-stream.Channel() + if !ok { + return errors.New("stream channel closed before getting the first message") + } + + if err := v.Err; err != nil { + return errors.Wrap(err, "remote error") + } + + events, err := a.parseFirstGetAttendantsMessage(v.Value.Bytes()) + if err != nil { + return errors.Wrap(err, "error parsing the first message") + } + + for _, event := range events { + select { + case ch <- event: + continue + case <-ctx.Done(): + return ctx.Err() + } + } + + for v := range stream.Channel() { + if err := v.Err; err != nil { + return errors.Wrap(err, "remote error") + } + + event, err := a.parseNextGetAttendantsMessage(v.Value.Bytes()) + if err != nil { + return errors.Wrap(err, "error parsing follow up messages") + } + + select { + case ch <- event: + continue + case <-ctx.Done(): + return ctx.Err() + } + } + + return errors.New("stream channel closed") +} + +func (a *PeerRPCAdapter) parseFirstGetAttendantsMessage(v []byte) ([]RoomAttendantsEvent, error) { + msg, err := messages.NewRoomAttendantsResponseStateFromBytes(v) + if err != nil { + return nil, errors.Wrap(err, "error parsing the message") + } + + var result []RoomAttendantsEvent + + for _, ref := range msg.Ids() { + event, err := NewRoomAttendantsEvent(RoomAttendantsEventTypeJoined, ref) + if err != nil { + return nil, errors.Wrap(err, "error creating the event") + } + + result = append(result, event) + } + + return result, nil +} + +func (a *PeerRPCAdapter) parseNextGetAttendantsMessage(v []byte) (RoomAttendantsEvent, error) { + msg, err := messages.NewRoomAttendantsResponseJoinedOrLeftFromBytes(v) + if err != nil { + return RoomAttendantsEvent{}, errors.Wrap(err, "error parsing the message") + } + + typ, err := NewRoomAttendantsEventTypeFromRoomAttendantsReponseType(msg.Typ()) + if err != nil { + return RoomAttendantsEvent{}, errors.Wrap(err, "error converting the type") + } + + event, err := NewRoomAttendantsEvent(typ, msg.Id()) + if err != nil { + return RoomAttendantsEvent{}, errors.Wrap(err, "error creating the event") + } + + return event, nil +} + +type RoomAttendantsEvent struct { + typ RoomAttendantsEventType + id refs.Identity +} + +func NewRoomAttendantsEvent(typ RoomAttendantsEventType, id refs.Identity) (RoomAttendantsEvent, error) { + if typ.IsZero() { + return RoomAttendantsEvent{}, errors.New("zero value of typ") + } + if id.IsZero() { + return RoomAttendantsEvent{}, errors.New("zero value of id") + } + return RoomAttendantsEvent{ + typ: typ, + id: id, + }, nil +} + +func MustNewRoomAttendantsEvent(typ RoomAttendantsEventType, id refs.Identity) RoomAttendantsEvent { + v, err := NewRoomAttendantsEvent(typ, id) + if err != nil { + panic(err) + } + return v +} + +func (e RoomAttendantsEvent) Typ() RoomAttendantsEventType { + return e.typ +} + +func (e RoomAttendantsEvent) Id() refs.Identity { + return e.id +} + +func (e RoomAttendantsEvent) IsZero() bool { + return e.id.IsZero() +} + +type RoomAttendantsEventType struct { + s string +} + +func NewRoomAttendantsEventTypeFromRoomAttendantsReponseType(v messages.RoomAttendantsResponseType) (RoomAttendantsEventType, error) { + switch v { + case messages.RoomAttendantsResponseTypeJoined: + return RoomAttendantsEventTypeJoined, nil + case messages.RoomAttendantsResponseTypeLeft: + return RoomAttendantsEventTypeLeft, nil + default: + return RoomAttendantsEventType{}, errors.New("unknown response type") + } +} + +func (t RoomAttendantsEventType) IsZero() bool { + return t == RoomAttendantsEventType{} +} + +var ( + RoomAttendantsEventTypeJoined = RoomAttendantsEventType{"joined"} + RoomAttendantsEventTypeLeft = RoomAttendantsEventType{"left"} +) diff --git a/service/domain/rooms/rpc_test.go b/service/domain/rooms/rpc_test.go new file mode 100644 index 00000000..1324f9b4 --- /dev/null +++ b/service/domain/rooms/rpc_test.go @@ -0,0 +1,101 @@ +package rooms_test + +import ( + "context" + "fmt" + "testing" + "time" + + "github.com/planetary-social/scuttlego/fixtures" + "github.com/planetary-social/scuttlego/service/domain/mocks" + "github.com/planetary-social/scuttlego/service/domain/refs" + "github.com/planetary-social/scuttlego/service/domain/rooms" + "github.com/planetary-social/scuttlego/service/domain/rooms/features" + "github.com/planetary-social/scuttlego/service/domain/transport" + "github.com/planetary-social/scuttlego/service/domain/transport/rpc" + "github.com/stretchr/testify/require" +) + +func TestGetMetadata(t *testing.T) { + ctx := fixtures.TestContext(t) + + conn := mocks.NewConnectionMock(ctx) + peer := transport.MustNewPeer(fixtures.SomePublicIdentity(), conn) + + conn.Mock(func(req *rpc.Request) []rpc.ResponseWithError { + return []rpc.ResponseWithError{ + { + Value: rpc.NewResponse([]byte(`{"membership": true, "features": ["tunnel", "some-other-feature"]}`)), + Err: nil, + }, + } + + }) + + ctx, cancel := context.WithTimeout(ctx, time.Second) + defer cancel() + + logger := fixtures.TestLogger(t) + adapter := rooms.NewPeerRPCAdapter(logger) + + metadata, err := adapter.GetMetadata(ctx, peer) + require.NoError(t, err) + + require.True(t, metadata.Membership()) + require.True(t, metadata.Features().Contains(features.FeatureTunnel)) +} + +func TestGetAttendants(t *testing.T) { + ctx := fixtures.TestContext(t) + + conn := mocks.NewConnectionMock(ctx) + peer := transport.MustNewPeer(fixtures.SomePublicIdentity(), conn) + + ref1 := refs.MustNewIdentity("@Uv38ByGCZU8WP18PmmIdcpVmx00QA3xNe7sEB9Hixkk=.ed25519") + ref2 := refs.MustNewIdentity("@gYVa2GgdDYbR6R4AFnk5y2aU0sQirNIIoAcpOUh/aZk=.ed25519") + ref3 := refs.MustNewIdentity("@650YpEeEBF2H88Z88idG6ZWvWiU2eVG6ov9s1HHEg/E=.ed25519") + ref4 := refs.MustNewIdentity("@YyUlP+xzjdep4ov5IRGcFg8HAkSGFbvaCDE/ao62aNI=.ed25519") + + conn.Mock(func(req *rpc.Request) []rpc.ResponseWithError { + return []rpc.ResponseWithError{ + { + Value: rpc.NewResponse([]byte(fmt.Sprintf(`{"type": "state", "ids": ["%s", "%s"]}`, ref1, ref2))), + Err: nil, + }, + { + Value: rpc.NewResponse([]byte(fmt.Sprintf(`{"type": "joined", "id": "%s"}`, ref3))), + Err: nil, + }, + { + Value: rpc.NewResponse([]byte(fmt.Sprintf(`{"type": "left", "id": "%s"}`, ref4))), + Err: nil, + }, + } + + }) + + ctx, cancel := context.WithTimeout(ctx, time.Second) + defer cancel() + + logger := fixtures.TestLogger(t) + adapter := rooms.NewPeerRPCAdapter(logger) + + ch, err := adapter.GetAttendants(ctx, peer) + require.NoError(t, err) + + var result []rooms.RoomAttendantsEvent + + for v := range ch { + result = append(result, v) + } + + require.Equal(t, + []rooms.RoomAttendantsEvent{ + rooms.MustNewRoomAttendantsEvent(rooms.RoomAttendantsEventTypeJoined, ref1), + rooms.MustNewRoomAttendantsEvent(rooms.RoomAttendantsEventTypeJoined, ref2), + rooms.MustNewRoomAttendantsEvent(rooms.RoomAttendantsEventTypeJoined, ref3), + rooms.MustNewRoomAttendantsEvent(rooms.RoomAttendantsEventTypeLeft, ref4), + }, + result, + ) +} diff --git a/service/domain/rooms/scanner.go b/service/domain/rooms/scanner.go new file mode 100644 index 00000000..10732a86 --- /dev/null +++ b/service/domain/rooms/scanner.go @@ -0,0 +1,87 @@ +package rooms + +import ( + "context" + + "github.com/boreq/errors" + "github.com/planetary-social/scuttlego/logging" + "github.com/planetary-social/scuttlego/service/domain/messages" + "github.com/planetary-social/scuttlego/service/domain/rooms/features" + "github.com/planetary-social/scuttlego/service/domain/transport" + "github.com/planetary-social/scuttlego/service/domain/transport/rpc" +) + +type MetadataGetter interface { + GetMetadata(ctx context.Context, peer transport.Peer) (messages.RoomMetadataResponse, error) +} + +type AttendantsGetter interface { + GetAttendants(ctx context.Context, peer transport.Peer) (<-chan RoomAttendantsEvent, error) +} + +type AttendantEventPublisher interface { + PublishAttendantEvent(portal transport.Peer, event RoomAttendantsEvent) error +} + +type Scanner struct { + metadataGetter MetadataGetter + attendantsGetter AttendantsGetter + publisher AttendantEventPublisher + logger logging.Logger +} + +func NewScanner( + metadataGetter MetadataGetter, + attendantsGetter AttendantsGetter, + publisher AttendantEventPublisher, + logger logging.Logger, +) *Scanner { + return &Scanner{ + metadataGetter: metadataGetter, + attendantsGetter: attendantsGetter, + publisher: publisher, + logger: logger.New("room_scanner"), + } +} + +func (s Scanner) Run(ctx context.Context, peer transport.Peer) error { + ok, err := s.canTunnelConnections(ctx, peer) + if err != nil { + return errors.Wrap(err, "error checking if the peer can tunnel connections") + } + + if !ok { + return nil + } + + attendants, err := s.attendantsGetter.GetAttendants(ctx, peer) + if err != nil { + return errors.Wrap(err, "failed to get attendants") + } + + for event := range attendants { + s.logger. + WithField("peer", peer). + WithField("event_type", event.typ). + WithField("event_ref", event.id.String()). + Debug("publishing an attendant event") + + if err := s.publisher.PublishAttendantEvent(peer, event); err != nil { + return errors.Wrap(err, "error publishing an event") + } + } + + return nil +} + +func (s Scanner) canTunnelConnections(ctx context.Context, peer transport.Peer) (bool, error) { + metadata, err := s.metadataGetter.GetMetadata(ctx, peer) + if err != nil { + if errors.Is(err, rpc.ErrRemoteError) { + return false, nil // most likely this is not a room at all + } + return false, errors.Wrap(err, "error getting metadata") + } + + return metadata.Features().Contains(features.FeatureTunnel), nil +} diff --git a/service/domain/rooms/scanner_test.go b/service/domain/rooms/scanner_test.go new file mode 100644 index 00000000..295ff771 --- /dev/null +++ b/service/domain/rooms/scanner_test.go @@ -0,0 +1,154 @@ +package rooms_test + +import ( + "context" + "testing" + + "github.com/planetary-social/scuttlego/fixtures" + "github.com/planetary-social/scuttlego/service/domain/messages" + "github.com/planetary-social/scuttlego/service/domain/mocks" + "github.com/planetary-social/scuttlego/service/domain/rooms" + "github.com/planetary-social/scuttlego/service/domain/rooms/features" + "github.com/planetary-social/scuttlego/service/domain/transport" + "github.com/stretchr/testify/require" +) + +func TestScanner_RunSimplyExitsIfTunnelingIsNotSupported(t *testing.T) { + ts := newTestScanner(t) + + ts.MetadataGetter.GetMetadataValue = messages.NewRoomMetadataResponse( + fixtures.SomeBool(), + features.Features{}, + ) + + ctx := fixtures.TestContext(t) + peer := transport.MustNewPeer(fixtures.SomePublicIdentity(), mocks.NewConnectionMock(ctx)) + + err := ts.Scanner.Run(ctx, peer) + require.NoError(t, err) + + require.Equal(t, []transport.Peer{peer}, ts.MetadataGetter.GetMetadataCalls) + require.Empty(t, ts.AttendantsGetter.GetAttendantsCalls) +} + +func TestScanner_RunSimplyGetsAttendantsAndPublishesEventsIfTunnelingIsSupported(t *testing.T) { + ts := newTestScanner(t) + + ts.MetadataGetter.GetMetadataValue = messages.NewRoomMetadataResponse( + fixtures.SomeBool(), + features.MustNewFeatures([]features.Feature{features.FeatureTunnel}), + ) + + event1 := rooms.MustNewRoomAttendantsEvent(rooms.RoomAttendantsEventTypeJoined, fixtures.SomeRefIdentity()) + event2 := rooms.MustNewRoomAttendantsEvent(rooms.RoomAttendantsEventTypeJoined, fixtures.SomeRefIdentity()) + + ts.AttendantsGetter.GetAttendantsValues = []rooms.RoomAttendantsEvent{ + event1, + event2, + } + + ctx := fixtures.TestContext(t) + peer := transport.MustNewPeer(fixtures.SomePublicIdentity(), mocks.NewConnectionMock(ctx)) + + err := ts.Scanner.Run(ctx, peer) + require.NoError(t, err) + + require.Equal(t, []transport.Peer{peer}, ts.MetadataGetter.GetMetadataCalls) + require.Equal(t, []transport.Peer{peer}, ts.AttendantsGetter.GetAttendantsCalls) + require.Equal(t, + []publishAttendantEventCall{ + { + Portal: peer, + Event: event1, + }, + { + Portal: peer, + Event: event2, + }, + }, + ts.Publisher.PublishAttendantEventCalls, + ) +} + +type testScanner struct { + Scanner *rooms.Scanner + MetadataGetter *metadataGetterMock + AttendantsGetter *attendantsGetterMock + Publisher *attendantEventPublisherMock +} + +func newTestScanner(t *testing.T) testScanner { + metadataGetter := newMetadataGetterMock() + attendantsGetter := newAttendantsGetterMock() + publisher := newAttendantEventPublisherMock() + logger := fixtures.TestLogger(t) + scanner := rooms.NewScanner(metadataGetter, attendantsGetter, publisher, logger) + + return testScanner{ + Scanner: scanner, + MetadataGetter: metadataGetter, + AttendantsGetter: attendantsGetter, + Publisher: publisher, + } +} + +type metadataGetterMock struct { + GetMetadataCalls []transport.Peer + GetMetadataValue messages.RoomMetadataResponse +} + +func newMetadataGetterMock() *metadataGetterMock { + return &metadataGetterMock{} +} + +func (m *metadataGetterMock) GetMetadata(ctx context.Context, peer transport.Peer) (messages.RoomMetadataResponse, error) { + m.GetMetadataCalls = append(m.GetMetadataCalls, peer) + return m.GetMetadataValue, nil +} + +type attendantsGetterMock struct { + GetAttendantsCalls []transport.Peer + GetAttendantsValues []rooms.RoomAttendantsEvent +} + +func newAttendantsGetterMock() *attendantsGetterMock { + return &attendantsGetterMock{} +} + +func (a *attendantsGetterMock) GetAttendants(ctx context.Context, peer transport.Peer) (<-chan rooms.RoomAttendantsEvent, error) { + a.GetAttendantsCalls = append(a.GetAttendantsCalls, peer) + ch := make(chan rooms.RoomAttendantsEvent) + go func() { + defer close(ch) + for _, value := range a.GetAttendantsValues { + select { + case <-ctx.Done(): + return + case ch <- value: + continue + } + } + }() + return ch, nil +} + +type attendantEventPublisherMock struct { + PublishAttendantEventCalls []publishAttendantEventCall +} + +func newAttendantEventPublisherMock() *attendantEventPublisherMock { + return &attendantEventPublisherMock{} +} + +func (a *attendantEventPublisherMock) PublishAttendantEvent(portal transport.Peer, event rooms.RoomAttendantsEvent) error { + a.PublishAttendantEventCalls = append(a.PublishAttendantEventCalls, publishAttendantEventCall{ + Portal: portal, + Event: event, + }) + return nil +} + +type publishAttendantEventCall struct { + Portal transport.Peer + Event rooms.RoomAttendantsEvent +} diff --git a/service/domain/rooms/tunnel/tunnel.go b/service/domain/rooms/tunnel/tunnel.go new file mode 100644 index 00000000..0fbebfca --- /dev/null +++ b/service/domain/rooms/tunnel/tunnel.go @@ -0,0 +1,108 @@ +package tunnel + +import ( + "bytes" + "context" + "io" + + "github.com/boreq/errors" + "github.com/planetary-social/scuttlego/service/domain/identity" + "github.com/planetary-social/scuttlego/service/domain/messages" + "github.com/planetary-social/scuttlego/service/domain/refs" + "github.com/planetary-social/scuttlego/service/domain/transport" + "github.com/planetary-social/scuttlego/service/domain/transport/rpc" +) + +type ClientPeerInitializer interface { + // InitializeClientPeer initializes outgoing connections by performing a handshake and establishing an RPC + // connection using the provided ReadWriteCloser. Context is used as the RPC connection context. + InitializeClientPeer(ctx context.Context, rwc io.ReadWriteCloser, remote identity.Public) (transport.Peer, error) +} + +type Dialer struct { + initializer ClientPeerInitializer +} + +func NewDialer(initializer ClientPeerInitializer) *Dialer { + return &Dialer{initializer: initializer} +} + +func (d *Dialer) DialViaRoom(ctx context.Context, portal transport.Peer, target identity.Public) (transport.Peer, error) { + // todo timeout? + portalRef, err := refs.NewIdentityFromPublic(portal.Identity()) + if err != nil { + return transport.Peer{}, errors.Wrap(err, "error creating portal identity ref") + } + + targetRef, err := refs.NewIdentityFromPublic(target) + if err != nil { + return transport.Peer{}, errors.Wrap(err, "error creating target identity ref") + } + + arguments, err := messages.NewTunnelConnectToPortalArguments(portalRef, targetRef) + if err != nil { + return transport.Peer{}, errors.Wrap(err, "error creating arguments") + } + + request, err := messages.NewTunnelConnectToPortal(arguments) + if err != nil { + return transport.Peer{}, errors.Wrap(err, "error creating a request") + } + + ctx, cancel := context.WithCancel(ctx) + + stream, err := portal.Conn().PerformRequest(ctx, request) + if err != nil { + cancel() + return transport.Peer{}, errors.Wrap(err, "error performing the request") + } + + rwc := NewStreamReadWriterCloserAdapter(stream, cancel) + peer, err := d.initializer.InitializeClientPeer(ctx, rwc, target) + if err != nil { + cancel() + return transport.Peer{}, errors.Wrap(err, "error performing the request") + } + + return peer, nil +} + +type StreamReadWriterCloserAdapter struct { + cancel context.CancelFunc + stream rpc.ResponseStream + buf *bytes.Buffer +} + +func NewStreamReadWriterCloserAdapter(stream rpc.ResponseStream, cancel context.CancelFunc) *StreamReadWriterCloserAdapter { + return &StreamReadWriterCloserAdapter{ + stream: stream, + cancel: cancel, + buf: &bytes.Buffer{}, + } +} + +func (s StreamReadWriterCloserAdapter) Read(p []byte) (int, error) { + if s.buf.Len() == 0 { + resp, ok := <-s.stream.Channel() + if !ok { + return 0, errors.New("channel closed") + } + + if err := resp.Err; err != nil { + return 0, errors.Wrap(err, "stream returned an error") + } + + s.buf.Write(resp.Value.Bytes()) + } + + return s.buf.Read(p) +} + +func (s StreamReadWriterCloserAdapter) Write(p []byte) (n int, err error) { + return len(p), s.stream.WriteMessage(p) +} + +func (s StreamReadWriterCloserAdapter) Close() error { + s.cancel() + return nil +} diff --git a/service/domain/rooms/tunnel/tunnel_test.go b/service/domain/rooms/tunnel/tunnel_test.go new file mode 100644 index 00000000..5cadb724 --- /dev/null +++ b/service/domain/rooms/tunnel/tunnel_test.go @@ -0,0 +1,288 @@ +package tunnel_test + +import ( + "bytes" + "context" + "fmt" + "io" + "testing" + "time" + + "github.com/boreq/errors" + "github.com/planetary-social/scuttlego/fixtures" + "github.com/planetary-social/scuttlego/service/domain/identity" + "github.com/planetary-social/scuttlego/service/domain/messages" + "github.com/planetary-social/scuttlego/service/domain/mocks" + "github.com/planetary-social/scuttlego/service/domain/rooms/tunnel" + "github.com/planetary-social/scuttlego/service/domain/transport" + "github.com/planetary-social/scuttlego/service/domain/transport/rpc" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestDialer_DialViaRoomPerformsCorrectRequestsAndCallsInitializerWithCorrectArguments(t *testing.T) { + clientPeerInitializer := newClientPeerInitializerMock() + dialer := tunnel.NewDialer(clientPeerInitializer) + + ctx := fixtures.TestContext(t) + portalConn := mocks.NewConnectionMock(ctx) + portalPeerRef := fixtures.SomeRefIdentity() + portalPeer := transport.MustNewPeer( + portalPeerRef.Identity(), + portalConn, + ) + targetRef := fixtures.SomeRefIdentity() + + someResponseBytes := fixtures.SomeBytes() + + portalConn.Mock(func(req *rpc.Request) []rpc.ResponseWithError { + assert.Equal(t, messages.TunnelConnectProcedure.Name(), req.Name()) + assert.Equal(t, messages.TunnelConnectProcedure.Typ(), req.Type()) + assert.Equal(t, + fmt.Sprintf(`[{"portal":"%s","target":"%s"}]`, portalPeerRef.String(), targetRef.String()), + string(req.Arguments()), + ) + + return []rpc.ResponseWithError{ + { + Value: rpc.NewResponse(someResponseBytes), + Err: nil, + }, + } + }) + + _, err := dialer.DialViaRoom(ctx, portalPeer, targetRef.Identity()) + require.NoError(t, err) + + require.Eventually(t, + func() bool { + if len(clientPeerInitializer.calls) > 0 { + fmt.Println(targetRef.Identity().String(), clientPeerInitializer.calls[0].Remote.String()) + fmt.Println(len(someResponseBytes), len(clientPeerInitializer.calls[0].ReceivedMessage)) + } + return assert.ObjectsAreEqual(clientPeerInitializer.calls, []clientPeerInitializerCall{ + { + Remote: targetRef.Identity(), + ReceivedMessage: someResponseBytes, + }, + }) + + }, 1*time.Second, 10*time.Millisecond) +} + +func TestStreamReadWriterCloserAdapter_CloseCallsCancel(t *testing.T) { + ctx := fixtures.TestContext(t) + cancel := newCancelFuncMock() + + stream := newResponseStreamMock(ctx, nil) + + adapter := tunnel.NewStreamReadWriterCloserAdapter(stream, cancel.Cancel) + + err := adapter.Close() + require.NoError(t, err) + require.True(t, cancel.Called) +} + +func TestStreamReadWriterCloserAdapter_ReadBlocksWaitingForData(t *testing.T) { + ctx := fixtures.TestContext(t) + cancel := newCancelFuncMock() + stream := newResponseStreamMock(ctx, nil) + adapter := tunnel.NewStreamReadWriterCloserAdapter(stream, cancel.Cancel) + + ch := make(chan struct{}) + go func() { + defer close(ch) + adapter.Read(nil) //nolint:errcheck + }() + + select { + case <-ch: + t.Fatal("read returned") + case <-time.After(100 * time.Millisecond): + t.Log("ok") + } +} + +func TestStreamReadWriterCloserAdapter_ReadPropagatesStreamErrors(t *testing.T) { + ctx := fixtures.TestContext(t) + cancel := newCancelFuncMock() + + stream := newResponseStreamMock(ctx, []rpc.ResponseWithError{ + { + Value: nil, + Err: errors.New("some error"), + }, + }) + + adapter := tunnel.NewStreamReadWriterCloserAdapter(stream, cancel.Cancel) + + _, err := adapter.Read(nil) + require.EqualError(t, err, "stream returned an error: some error") +} + +func TestStreamReadWriterCloserAdapter_SmallMessagesAreRetrievedInOneRead(t *testing.T) { + ctx := fixtures.TestContext(t) + cancel := newCancelFuncMock() + + const bufSize = 100 + msg := bytes.Repeat([]byte("a"), bufSize/2) + + stream := newResponseStreamMock(ctx, []rpc.ResponseWithError{ + { + Value: rpc.NewResponse(msg), + Err: nil, + }, + }) + + adapter := tunnel.NewStreamReadWriterCloserAdapter(stream, cancel.Cancel) + + buf := make([]byte, bufSize) + + n, err := adapter.Read(buf) + require.NoError(t, err) + require.Equal(t, len(msg), n) + require.Equal(t, msg, buf[:n]) +} + +func TestStreamReadWriterCloserAdapter_LargeMessagesAreRetrievedOverMultipleReads(t *testing.T) { + ctx := fixtures.TestContext(t) + cancel := newCancelFuncMock() + + const bufSize = 100 + + first := bytes.Repeat([]byte("a"), bufSize) + second := bytes.Repeat([]byte("b"), bufSize) + + var responseBytes []byte + responseBytes = append(responseBytes, first...) + responseBytes = append(responseBytes, second...) + + stream := newResponseStreamMock(ctx, []rpc.ResponseWithError{ + { + Value: rpc.NewResponse(responseBytes), + Err: nil, + }, + }) + + adapter := tunnel.NewStreamReadWriterCloserAdapter(stream, cancel.Cancel) + + buf := make([]byte, bufSize) + + n, err := adapter.Read(buf) + require.NoError(t, err) + require.Equal(t, 100, n) + require.Equal(t, first, buf) + + n, err = adapter.Read(buf) + require.NoError(t, err) + require.Equal(t, 100, n) + require.Equal(t, second, buf) +} + +func TestStreamReadWriterCloserAdapter_ReadWhenChannelIsClosedReturnsAnError(t *testing.T) { + ctx := fixtures.TestContext(t) + cancel := newCancelFuncMock() + + streamCtx, streamCtxCancel := context.WithCancel(ctx) + streamCtxCancel() + stream := newResponseStreamMock(streamCtx, nil) + + adapter := tunnel.NewStreamReadWriterCloserAdapter(stream, cancel.Cancel) + + _, err := adapter.Read(nil) + require.EqualError(t, err, "channel closed") +} + +func TestStreamReadWriterCloserAdapter_WriteCallsWriteMessage(t *testing.T) { + ctx := fixtures.TestContext(t) + cancel := newCancelFuncMock() + stream := newResponseStreamMock(ctx, nil) + adapter := tunnel.NewStreamReadWriterCloserAdapter(stream, cancel.Cancel) + + someBytes := fixtures.SomeBytes() + + n, err := adapter.Write(someBytes) + require.NoError(t, err) + require.Equal(t, len(someBytes), n) + require.Equal(t, + [][]byte{ + someBytes, + }, + stream.WriteMessageCalls, + ) +} + +type responseStreamMock struct { + ch chan rpc.ResponseWithError + WriteMessageCalls [][]byte +} + +func newResponseStreamMock(ctx context.Context, messagesToReceive []rpc.ResponseWithError) *responseStreamMock { + ch := make(chan rpc.ResponseWithError) + go func() { + defer close(ch) + + for _, msgToReceive := range messagesToReceive { + select { + case ch <- msgToReceive: + continue + case <-ctx.Done(): + return + } + } + + <-ctx.Done() + }() + return &responseStreamMock{ + ch: ch, + } +} + +func (r *responseStreamMock) WriteMessage(body []byte) error { + r.WriteMessageCalls = append(r.WriteMessageCalls, body) + return nil +} + +func (r *responseStreamMock) Channel() <-chan rpc.ResponseWithError { + return r.ch +} + +type cancelFuncMock struct { + Called bool +} + +func newCancelFuncMock() *cancelFuncMock { + return &cancelFuncMock{} +} + +func (m *cancelFuncMock) Cancel() { + m.Called = true +} + +type clientPeerInitializerMock struct { + calls []clientPeerInitializerCall +} + +func newClientPeerInitializerMock() *clientPeerInitializerMock { + return &clientPeerInitializerMock{} +} + +func (c *clientPeerInitializerMock) InitializeClientPeer(ctx context.Context, rwc io.ReadWriteCloser, remote identity.Public) (transport.Peer, error) { + buf := make([]byte, 10000) + n, err := rwc.Read(buf) + if err != nil { + return transport.Peer{}, err + } + + c.calls = append(c.calls, clientPeerInitializerCall{ + ReceivedMessage: buf[:n], + Remote: remote, + }) + + return transport.Peer{}, nil +} + +type clientPeerInitializerCall struct { + Remote identity.Public + ReceivedMessage []byte +} diff --git a/service/domain/transport/boxstream/handshake.go b/service/domain/transport/boxstream/handshake.go index f42672d4..ba6b16b0 100644 --- a/service/domain/transport/boxstream/handshake.go +++ b/service/domain/transport/boxstream/handshake.go @@ -2,21 +2,39 @@ package boxstream import ( "io" + "time" "github.com/boreq/errors" "github.com/planetary-social/scuttlego/service/domain/identity" "go.cryptoscope.co/secretstream/secrethandshake" ) +const ( + handshakeTimeout = 15 * time.Second +) + +type CurrentTimeProvider interface { + Get() time.Time +} + +type SetDeadliner interface { + SetDeadline(t time.Time) error +} + // Handshaker performs the Secret Handshake using the provided ReadWriteCloser. type Handshaker struct { - local identity.Private - networkKey NetworkKey + local identity.Private + networkKey NetworkKey + currentTimeProvider CurrentTimeProvider } // NewHandshaker creates a new handshaker which uses the provided local private // identity when performing secret handshakes. -func NewHandshaker(local identity.Private, networkKey NetworkKey) (Handshaker, error) { +func NewHandshaker( + local identity.Private, + networkKey NetworkKey, + currentTimeProvider CurrentTimeProvider, +) (Handshaker, error) { if local.IsZero() { return Handshaker{}, errors.New("zero value of private identity") } @@ -26,8 +44,9 @@ func NewHandshaker(local identity.Private, networkKey NetworkKey) (Handshaker, e } return Handshaker{ - local: local, - networkKey: networkKey, + local: local, + networkKey: networkKey, + currentTimeProvider: currentTimeProvider, }, nil } @@ -35,6 +54,12 @@ func NewHandshaker(local identity.Private, networkKey NetworkKey) (Handshaker, e // remote peer and the provided ReadWriteCloser. This should be used when // initiating a connection with a remote peer. func (h Handshaker) OpenClientStream(rw io.ReadWriteCloser, remote identity.Public) (*Stream, error) { + if v, ok := rw.(SetDeadliner); ok { + if err := v.SetDeadline(h.currentTimeProvider.Get().Add(handshakeTimeout)); err != nil { + return nil, errors.Wrap(err, "failed to set a deadline") + } + } + if remote.IsZero() { return nil, errors.New("zero value of remote identity") } @@ -49,6 +74,12 @@ func (h Handshaker) OpenClientStream(rw io.ReadWriteCloser, remote identity.Publ return nil, errors.Wrap(err, "could not perform the client handshake") } + if v, ok := rw.(SetDeadliner); ok { + if err := v.SetDeadline(time.Time{}); err != nil { + return nil, errors.Wrap(err, "failed to reset a deadline") + } + } + return h.createStream(rw, state) } diff --git a/service/domain/transport/boxstream/handshake_test.go b/service/domain/transport/boxstream/handshake_test.go index da3a948f..d36226b7 100644 --- a/service/domain/transport/boxstream/handshake_test.go +++ b/service/domain/transport/boxstream/handshake_test.go @@ -7,31 +7,67 @@ import ( "github.com/hashicorp/go-multierror" "github.com/planetary-social/scuttlego/fixtures" + "github.com/planetary-social/scuttlego/service/adapters/mocks" "github.com/planetary-social/scuttlego/service/domain/transport/boxstream" "github.com/stretchr/testify/require" ) -func TestHandshaker(t *testing.T) { +func TestHandshaker_ConnectionDoesNotImplementSetDeadliner(t *testing.T) { t.Parallel() + networkKey := boxstream.NewDefaultNetworkKey() + currentTimeProvider := mocks.NewCurrentTimeProviderMock() - peer1 := fixtures.SomePrivateIdentity() - handshaker1, err := boxstream.NewHandshaker(peer1, networkKey) - require.NoError(t, err) + oneToTwoReader, oneToTwoWriter := io.Pipe() + twoToOneReader, twoToOneWriter := io.Pipe() - peer2 := fixtures.SomePrivateIdentity() - handshaker2, err := boxstream.NewHandshaker(peer2, networkKey) - require.NoError(t, err) + conn1 := newMockReadWriteCloser(twoToOneReader, oneToTwoWriter) + defer conn1.Close() + + conn2 := newMockReadWriteCloser(oneToTwoReader, twoToOneWriter) + defer conn2.Close() + + runTest(t, networkKey, currentTimeProvider, conn1, conn2) +} + +func TestHandshaker_HandshakerSetsDeadlinesIfConnectionImplementsSetDeadliner(t *testing.T) { + t.Parallel() + + networkKey := boxstream.NewDefaultNetworkKey() + currentTimeProvider := mocks.NewCurrentTimeProviderMock() + currentTimeProvider.CurrentTime = time.Now() oneToTwoReader, oneToTwoWriter := io.Pipe() twoToOneReader, twoToOneWriter := io.Pipe() - conn1 := newMockConnection(twoToOneReader, oneToTwoWriter) + conn1 := newReadWriteCloseSetDeadliner(twoToOneReader, oneToTwoWriter) defer conn1.Close() - conn2 := newMockConnection(oneToTwoReader, twoToOneWriter) + conn2 := newReadWriteCloseSetDeadliner(oneToTwoReader, twoToOneWriter) defer conn2.Close() + runTest(t, networkKey, currentTimeProvider, conn1, conn2) + + require.Equal(t, + []time.Time{ + currentTimeProvider.CurrentTime.Add(15 * time.Second), + {}, + }, + conn1.SetDeadlineCalls, + ) + + require.Empty(t, conn2.SetDeadlineCalls) +} + +func runTest(t *testing.T, networkKey boxstream.NetworkKey, currentTimeProvider *mocks.CurrentTimeProviderMock, conn1 io.ReadWriteCloser, conn2 io.ReadWriteCloser) { + peer1 := fixtures.SomePrivateIdentity() + handshaker1, err := boxstream.NewHandshaker(peer1, networkKey, currentTimeProvider) + require.NoError(t, err) + + peer2 := fixtures.SomePrivateIdentity() + handshaker2, err := boxstream.NewHandshaker(peer2, networkKey, currentTimeProvider) + require.NoError(t, err) + errCh := make(chan error) var stream1 *boxstream.Stream @@ -61,31 +97,48 @@ func TestHandshaker(t *testing.T) { // test reading and writing to confirm that secrets are set correctly testWriteRead(t, stream1, stream2, []byte("test")) testWriteRead(t, stream2, stream1, []byte("test")) + } -type mockConnection struct { +type mockReadWriteCloser struct { read io.ReadCloser write io.WriteCloser } -func newMockConnection(read io.ReadCloser, write io.WriteCloser) *mockConnection { - return &mockConnection{ +func newMockReadWriteCloser(read io.ReadCloser, write io.WriteCloser) *mockReadWriteCloser { + return &mockReadWriteCloser{ read: read, write: write, } } -func (m mockConnection) Read(p []byte) (n int, err error) { +func (m mockReadWriteCloser) Read(p []byte) (n int, err error) { return m.read.Read(p) } -func (m mockConnection) Write(p []byte) (n int, err error) { +func (m mockReadWriteCloser) Write(p []byte) (n int, err error) { return m.write.Write(p) } -func (m mockConnection) Close() error { +func (m mockReadWriteCloser) Close() error { var err error err = multierror.Append(err, m.read.Close()) err = multierror.Append(err, m.write.Close()) return err } + +type mockReadWriteCloseSetDeadliner struct { + *mockReadWriteCloser + SetDeadlineCalls []time.Time +} + +func newReadWriteCloseSetDeadliner(read io.ReadCloser, write io.WriteCloser) *mockReadWriteCloseSetDeadliner { + return &mockReadWriteCloseSetDeadliner{ + mockReadWriteCloser: newMockReadWriteCloser(read, write), + } +} + +func (m *mockReadWriteCloseSetDeadliner) SetDeadline(t time.Time) error { + m.SetDeadlineCalls = append(m.SetDeadlineCalls, t) + return nil +} diff --git a/service/domain/transport/boxstream/stream_test.go b/service/domain/transport/boxstream/stream_test.go index 1b64597b..59a969e6 100644 --- a/service/domain/transport/boxstream/stream_test.go +++ b/service/domain/transport/boxstream/stream_test.go @@ -49,10 +49,10 @@ func newStreams(t *testing.T) (*boxstream.Stream, *boxstream.Stream) { twoToOneReader, twoToOneWriter := io.Pipe() identity1 := fixtures.SomePublicIdentity() - conn1 := newMockConnection(twoToOneReader, oneToTwoWriter) + conn1 := newMockReadWriteCloser(twoToOneReader, oneToTwoWriter) identity2 := fixtures.SomePublicIdentity() - conn2 := newMockConnection(oneToTwoReader, twoToOneWriter) + conn2 := newMockReadWriteCloser(oneToTwoReader, twoToOneWriter) stream1, err := boxstream.NewStream(conn1, boxstream.HandshakeResult{ Remote: identity2, diff --git a/service/domain/transport/peer.go b/service/domain/transport/peer.go index a8523710..447df7c0 100644 --- a/service/domain/transport/peer.go +++ b/service/domain/transport/peer.go @@ -6,6 +6,7 @@ import ( "context" "fmt" + "github.com/boreq/errors" "github.com/planetary-social/scuttlego/service/domain/identity" "github.com/planetary-social/scuttlego/service/domain/refs" "github.com/planetary-social/scuttlego/service/domain/transport/rpc" @@ -36,11 +37,27 @@ type Peer struct { conn Connection } -func NewPeer(remote identity.Public, conn Connection) Peer { +func NewPeer(remote identity.Public, conn Connection) (Peer, error) { + if remote.IsZero() { + return Peer{}, errors.New("zero value of remote identity") + } + + if conn == nil { + return Peer{}, errors.New("conn is nil") + } + return Peer{ remote: remote, conn: conn, + }, nil +} + +func MustNewPeer(remote identity.Public, conn Connection) Peer { + v, err := NewPeer(remote, conn) + if err != nil { + panic(err) } + return v } func (p Peer) Identity() identity.Public { @@ -51,6 +68,10 @@ func (p Peer) Conn() Connection { return p.conn } +func (p Peer) IsZero() bool { + return p.conn == nil +} + func (p Peer) String() string { public, _ := refs.NewIdentityFromPublic(p.remote) return fmt.Sprintf("", public.String(), p.conn) diff --git a/service/domain/transport/peer_initializer.go b/service/domain/transport/peer_initializer.go index 913e4367..2d37e1f7 100644 --- a/service/domain/transport/peer_initializer.go +++ b/service/domain/transport/peer_initializer.go @@ -69,7 +69,12 @@ func (i PeerInitializer) initializePeer(ctx context.Context, boxStream *boxstrea return Peer{}, errors.Wrap(err, "failed to establish an RPC connection") } - return NewPeer(boxStream.Remote(), rpcConn), nil + peer, err := NewPeer(boxStream.Remote(), rpcConn) + if err != nil { + return Peer{}, errors.Wrap(err, "error creating a peer") + } + + return peer, nil } func (i PeerInitializer) peerLogger(boxStream *boxstream.Stream) (logging.Logger, error) { diff --git a/service/domain/transport/rpc/connection.go b/service/domain/transport/rpc/connection.go index e98f353b..118517dd 100644 --- a/service/domain/transport/rpc/connection.go +++ b/service/domain/transport/rpc/connection.go @@ -2,6 +2,7 @@ package rpc import ( "context" + "fmt" "github.com/boreq/errors" "github.com/planetary-social/scuttlego/logging" @@ -26,6 +27,7 @@ type Connection struct { requestStreams *RequestStreams logger logging.Logger + id ConnectionId } // NewConnection is the only way of creating a new Connection, zero value is invalid. Terminating the provided context @@ -50,7 +52,8 @@ func NewConnection( raw: raw, responseStreams: NewResponseStreams(raw, logger), requestStreams: NewRequestStreams(ctx, raw, handler, logger), - logger: logger.New("connection"), + logger: logger.WithField("id", id).New("connection"), + id: id, } go func() { @@ -89,6 +92,10 @@ func (c *Connection) WasInitiatedByRemote() bool { return c.wasInitiatedByRemote } +func (c *Connection) String() string { + return fmt.Sprintf("", c.id, c.wasInitiatedByRemote) +} + func (c *Connection) readLoop() { defer c.cancel() diff --git a/service/domain/transport/rpc/response_streams.go b/service/domain/transport/rpc/response_streams.go index 7565e601..fca56639 100644 --- a/service/domain/transport/rpc/response_streams.go +++ b/service/domain/transport/rpc/response_streams.go @@ -30,6 +30,7 @@ var ( ErrRemoteError = errors.New("remote error") ) +// todo private fields and constructor type ResponseWithError struct { // Value is only set if Err is nil. Value *Response diff --git a/service/domain/transport/rpc/transport/raw_connection.go b/service/domain/transport/rpc/transport/raw_connection.go index a8ef3ca0..d2946b1e 100644 --- a/service/domain/transport/rpc/transport/raw_connection.go +++ b/service/domain/transport/rpc/transport/raw_connection.go @@ -85,11 +85,14 @@ func (s RawConnection) Close() error { } func (s RawConnection) loggerWithMessageFields(msg *Message) logging.Logger { - return s.logger. + l := s.logger. WithField("header.flags", msg.Header.Flags()). WithField("header.number", msg.Header.RequestNumber()). - WithField("header.bodyLength", msg.Header.BodyLength()). - WithField("body", string(msg.Body)) + WithField("header.bodyLength", msg.Header.BodyLength()) + if msg.Header.Flags().BodyType() != MessageBodyTypeBinary { + l = l.WithField("body", string(msg.Body)) + } + return l } func isTermination(bytes []byte) bool { diff --git a/service/ports/pubsub/requests.go b/service/ports/pubsub/requests.go index 93dbd460..c0166501 100644 --- a/service/ports/pubsub/requests.go +++ b/service/ports/pubsub/requests.go @@ -35,9 +35,7 @@ func NewRequestSubscriber(pubsub *pubsub.RequestPubSub, mux *mux.Mux) *RequestSu // Run keeps receiving RPC request from the pubsub and passing them to the RPC // mux until the context is closed. func (p *RequestSubscriber) Run(ctx context.Context) error { - requests := p.pubsub.SubscribeToRequests(ctx) - - for request := range requests { + for request := range p.pubsub.SubscribeToRequests(ctx) { p.mux.HandleRequest(request.Ctx, request.Stream, request.Req) } diff --git a/service/ports/pubsub/room_attendant_events.go b/service/ports/pubsub/room_attendant_events.go new file mode 100644 index 00000000..d9882320 --- /dev/null +++ b/service/ports/pubsub/room_attendant_events.go @@ -0,0 +1,49 @@ +// Package pubsub receives internal events. +package pubsub + +import ( + "context" + + "github.com/planetary-social/scuttlego/logging" + "github.com/planetary-social/scuttlego/service/adapters/pubsub" + "github.com/planetary-social/scuttlego/service/app/commands" +) + +type ProcessRoomAttendantEventHandler interface { + Handle(cmd commands.ProcessRoomAttendantEvent) error +} + +type RoomAttendantEventSubscriber struct { + pubsub *pubsub.RoomAttendantEventPubSub + handler ProcessRoomAttendantEventHandler + logger logging.Logger +} + +func NewRoomAttendantEventSubscriber( + pubsub *pubsub.RoomAttendantEventPubSub, + handler ProcessRoomAttendantEventHandler, + logger logging.Logger, +) *RoomAttendantEventSubscriber { + return &RoomAttendantEventSubscriber{ + pubsub: pubsub, + handler: handler, + logger: logger.New("room_attendant_event_subscriber"), + } +} + +func (p *RoomAttendantEventSubscriber) Run(ctx context.Context) error { + for event := range p.pubsub.SubscribeToAttendantEvents(ctx) { + cmd, err := commands.NewProcessRoomAttendantEvent(event.Portal, event.Event) + if err != nil { + p.logger.WithError(err).Debug("error creating the command") + continue + } + + if err := p.handler.Handle(cmd); err != nil { + p.logger.WithError(err).Debug("error handling the command") + continue + } + } + + return nil +} diff --git a/service/ports/pubsub/room_attendant_events_test.go b/service/ports/pubsub/room_attendant_events_test.go new file mode 100644 index 00000000..2582be4d --- /dev/null +++ b/service/ports/pubsub/room_attendant_events_test.go @@ -0,0 +1,71 @@ +package pubsub_test + +import ( + "sync" + "testing" + "time" + + "github.com/planetary-social/scuttlego/fixtures" + pubsub2 "github.com/planetary-social/scuttlego/service/adapters/pubsub" + "github.com/planetary-social/scuttlego/service/app/commands" + "github.com/planetary-social/scuttlego/service/domain/mocks" + "github.com/planetary-social/scuttlego/service/domain/rooms" + "github.com/planetary-social/scuttlego/service/domain/transport" + "github.com/planetary-social/scuttlego/service/ports/pubsub" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestRoomAttendantEventSubscriber_ReceivesEventsAndCallsTheCommandHandler(t *testing.T) { + ctx := fixtures.TestContext(t) + + ps := pubsub2.NewRoomAttendantEventPubSub() + logger := fixtures.TestLogger(t) + handler := newProcessRoomAttendantEventHandlerMock() + + subscriber := pubsub.NewRoomAttendantEventSubscriber( + ps, + handler, + logger, + ) + go subscriber.Run(ctx) //nolint:errcheck + + portal := transport.MustNewPeer(fixtures.SomePublicIdentity(), mocks.NewConnectionMock(ctx)) + event, err := rooms.NewRoomAttendantsEvent(rooms.RoomAttendantsEventTypeJoined, fixtures.SomeRefIdentity()) + require.NoError(t, err) + + err = ps.PublishAttendantEvent(portal, event) + require.NoError(t, err) + + cmd, err := commands.NewProcessRoomAttendantEvent(portal, event) + require.NoError(t, err) + + require.Eventually(t, + func() bool { + return assert.ObjectsAreEqual([]commands.ProcessRoomAttendantEvent{cmd}, handler.Calls()) + }, 1*time.Second, 10*time.Millisecond) +} + +type processRoomAttendantEventHandlerMock struct { + lock sync.Mutex + calls []commands.ProcessRoomAttendantEvent +} + +func newProcessRoomAttendantEventHandlerMock() *processRoomAttendantEventHandlerMock { + return &processRoomAttendantEventHandlerMock{} +} + +func (p *processRoomAttendantEventHandlerMock) Handle(cmd commands.ProcessRoomAttendantEvent) error { + p.lock.Lock() + defer p.lock.Unlock() + p.calls = append(p.calls, cmd) + return nil +} + +func (p *processRoomAttendantEventHandlerMock) Calls() []commands.ProcessRoomAttendantEvent { + p.lock.Lock() + defer p.lock.Unlock() + tmp := make([]commands.ProcessRoomAttendantEvent, len(p.calls)) + copy(tmp, p.calls) + return tmp +}