From a4051ad0212aa07b1365ccc4eec57eb766ffbe28 Mon Sep 17 00:00:00 2001 From: David Gerber Date: Tue, 28 Sep 2021 20:26:28 +0200 Subject: [PATCH] Initial commit --- .editorconfig | 672 + .gitignore | 9 + .jpb/persistence-units.xml | 12 + LICENSE | 674 + README.md | 117 + app/.gitignore | 2 + app/build.gradle | 130 + .../java/io/xeres/app/XeresApplication.java | 59 + .../app/application/SingleInstanceRun.java | 108 + .../io/xeres/app/application/Startup.java | 295 + .../app/application/environment/Cloud.java | 49 + .../environment/CommandArgument.java | 181 + .../application/environment/HostVariable.java | 117 + .../events/LocationReadyEvent.java | 28 + .../app/application/events/package-info.java | 26 + .../configuration/DataDirConfiguration.java | 142 + .../DataSourceConfiguration.java | 85 + .../configuration/SchedulerConfiguration.java | 32 + .../configuration/WebSocketConfiguration.java | 77 + .../app/crypto/chatcipher/ChatChallenge.java | 52 + .../java/io/xeres/app/crypto/pgp/PGP.java | 303 + .../io/xeres/app/crypto/pgp/PGPSigner.java | 74 + .../io/xeres/app/crypto/pgp/package-info.java | 29 + .../java/io/xeres/app/crypto/rsa/RSA.java | 131 + .../java/io/xeres/app/crypto/rsid/RSId.java | 153 + .../io/xeres/app/crypto/rsid/RSIdArmor.java | 226 + .../io/xeres/app/crypto/rsid/RSIdCrc.java | 47 + .../app/crypto/rsid/RSSerialVersion.java | 66 + .../rsid/certificate/RSCertificate.java | 345 + .../rsid/certificate/RSCertificateTags.java | 39 + .../crypto/rsid/shortinvite/ShortInvite.java | 312 + .../rsid/shortinvite/ShortInviteQuirks.java | 54 + .../rsid/shortinvite/ShortInviteTags.java | 38 + .../app/crypto/scramble/ScrambledString.java | 306 + .../java/io/xeres/app/crypto/x509/X509.java | 138 + .../xeres/app/database/DatabaseSession.java | 56 + .../app/database/DatabaseSessionManager.java | 56 + .../database/converter/EnumSetConverter.java | 62 + .../converter/GxsPrivacyFlagsConverter.java | 34 + .../converter/GxsSignatureFlagsConverter.java | 34 + .../app/database/model/chatroom/ChatRoom.java | 151 + .../database/model/connection/Connection.java | 201 + .../model/connection/ConnectionMapper.java | 59 + .../app/database/model/gxs/GxsCircleType.java | 31 + .../database/model/gxs/GxsClientUpdate.java | 107 + .../app/database/model/gxs/GxsGroupItem.java | 494 + .../database/model/gxs/GxsMessageItem.java | 24 + .../database/model/gxs/GxsPrivacyFlags.java | 33 + .../database/model/gxs/GxsServiceSetting.java | 74 + .../database/model/gxs/GxsSignatureFlags.java | 28 + .../app/database/model/identity/Identity.java | 115 + .../app/database/model/location/Location.java | 352 + .../model/location/LocationMapper.java | 83 + .../xeres/app/database/model/prefs/Prefs.java | 88 + .../app/database/model/profile/Profile.java | 241 + .../database/model/profile/ProfileMapper.java | 112 + .../repository/ChatRoomRepository.java | 42 + .../repository/GxsClientUpdateRepository.java | 33 + .../database/repository/GxsIdRepository.java | 33 + .../GxsServiceSettingRepository.java | 29 + .../repository/IdentityRepository.java | 30 + .../repository/LocationRepository.java | 46 + .../database/repository/PrefsRepository.java | 35 + .../repository/ProfileRepository.java | 50 + .../io/xeres/app/job/PeerConnectionJob.java | 78 + .../net/bdisc/BroadcastDiscoveryService.java | 356 + .../xeres/app/net/bdisc/UdpDiscoveryPeer.java | 149 + .../app/net/bdisc/UdpDiscoveryProtocol.java | 118 + .../java/io/xeres/app/net/dht/DHTService.java | 220 + .../io/xeres/app/net/dht/DHTSpringLog.java | 83 + .../io/xeres/app/net/dht/package-info.java | 28 + .../app/net/peer/ConnectionDirection.java | 26 + .../io/xeres/app/net/peer/PeerAttribute.java | 33 + .../io/xeres/app/net/peer/PeerConnection.java | 136 + .../app/net/peer/PeerConnectionManager.java | 189 + .../app/net/peer/bootstrap/PeerClient.java | 120 + .../net/peer/bootstrap/PeerInitializer.java | 107 + .../app/net/peer/bootstrap/PeerServer.java | 143 + .../app/net/peer/packet/MultiPacket.java | 175 + .../io/xeres/app/net/peer/packet/Packet.java | 152 + .../app/net/peer/packet/SimplePacket.java | 57 + .../app/net/peer/packet/package-info.java | 43 + .../net/peer/pipeline/IdleEventHandler.java | 66 + .../app/net/peer/pipeline/ItemDecoder.java | 96 + .../app/net/peer/pipeline/ItemEncoder.java | 39 + .../net/peer/pipeline/MultiPacketEncoder.java | 182 + .../app/net/peer/pipeline/PacketDecoder.java | 72 + .../app/net/peer/pipeline/PeerHandler.java | 204 + .../peer/pipeline/SimplePacketEncoder.java | 36 + .../app/net/peer/pipeline/package-info.java | 29 + .../java/io/xeres/app/net/peer/ssl/SSL.java | 146 + .../xeres/app/net/protocol/PeerAddress.java | 441 + .../app/net/protocol/tor/OnionAddress.java | 37 + .../app/net/protocol/tor/package-info.java | 23 + .../io/xeres/app/net/tou/package-info.java | 24 + .../io/xeres/app/net/upnp/ControlPoint.java | 220 + .../java/io/xeres/app/net/upnp/Device.java | 387 + .../io/xeres/app/net/upnp/DeviceSpecs.java | 63 + .../io/xeres/app/net/upnp/HttpuHeader.java | 28 + .../io/xeres/app/net/upnp/PortMapping.java | 60 + .../java/io/xeres/app/net/upnp/Protocol.java | 26 + .../main/java/io/xeres/app/net/upnp/Soap.java | 92 + .../io/xeres/app/net/upnp/UPNPService.java | 347 + .../io/xeres/app/net/upnp/package-info.java | 40 + .../app/properties/DatabaseProperties.java | 40 + .../app/properties/NetworkProperties.java | 91 + .../io/xeres/app/properties/UiProperties.java | 64 + .../io/xeres/app/service/ChatRoomService.java | 86 + .../xeres/app/service/GxsExchangeService.java | 78 + .../io/xeres/app/service/IdentityService.java | 205 + .../io/xeres/app/service/LocationService.java | 346 + .../io/xeres/app/service/PeerService.java | 85 + .../io/xeres/app/service/PrefsService.java | 125 + .../io/xeres/app/service/ProfileService.java | 214 + .../io/xeres/app/web/api/DefaultHandler.java | 89 + .../api/controller/chat/ChatController.java | 79 + .../chat/ChatMessageController.java | 96 + .../controller/config/ConfigController.java | 192 + .../connection/ConnectionController.java | 62 + .../location/LocationController.java | 76 + .../notification/NotificationController.java | 59 + .../controller/profile/ProfileController.java | 154 + .../io/xeres/app/web/api/error/Error.java | 70 + .../web/api/error/ErrorResponseEntity.java | 113 + .../InternalServerErrorException.java | 28 + .../UnprocessableEntityException.java | 41 + .../api/sse/SsePushNotificationService.java | 63 + .../io/xeres/app/xrs/common/SecurityKey.java | 89 + .../xeres/app/xrs/common/SecurityKeySet.java | 67 + .../io/xeres/app/xrs/common/Signature.java | 45 + .../io/xeres/app/xrs/common/SignatureSet.java | 68 + .../main/java/io/xeres/app/xrs/item/Item.java | 108 + .../io/xeres/app/xrs/item/ItemFactory.java | 54 + .../io/xeres/app/xrs/item/ItemPriority.java | 22 + .../java/io/xeres/app/xrs/item/RawItem.java | 143 + .../serialization/AnnotationSerializer.java | 109 + .../xrs/serialization/ArraySerializer.java | 54 + .../xrs/serialization/BooleanSerializer.java | 52 + .../serialization/ByteArraySerializer.java | 74 + .../app/xrs/serialization/ByteSerializer.java | 52 + .../xrs/serialization/DoubleSerializer.java | 52 + .../app/xrs/serialization/EnumSerializer.java | 53 + .../xrs/serialization/EnumSetSerializer.java | 181 + .../app/xrs/serialization/FieldSize.java | 27 + .../xrs/serialization/FloatSerializer.java | 52 + .../serialization/IdentifierSerializer.java | 96 + .../app/xrs/serialization/IntSerializer.java | 52 + .../app/xrs/serialization/ListSerializer.java | 79 + .../app/xrs/serialization/LongSerializer.java | 52 + .../app/xrs/serialization/MapSerializer.java | 144 + .../app/xrs/serialization/RsSerializable.java | 31 + .../RsSerializableSerializer.java | 52 + .../app/xrs/serialization/RsSerialized.java | 35 + .../xrs/serialization/SerializationFlags.java | 27 + .../app/xrs/serialization/Serializer.java | 666 + .../xrs/serialization/ShortSerializer.java | 52 + .../xrs/serialization/StringSerializer.java | 59 + .../serialization/TlvAddressSerializer.java | 161 + .../serialization/TlvBinarySerializer.java | 95 + .../TlvSecurityKeySerializer.java | 84 + .../TlvSecurityKeySetSerializer.java | 82 + .../app/xrs/serialization/TlvSerializer.java | 104 + .../xrs/serialization/TlvSetSerializer.java | 70 + .../serialization/TlvSignatureSerializer.java | 71 + .../TlvSignatureSetSerializer.java | 77 + .../serialization/TlvStringSerializer.java | 67 + .../TlvStringSetRefSerializer.java | 69 + .../xeres/app/xrs/serialization/TlvType.java | 63 + .../serialization/TlvUint32Serializer.java | 62 + .../xeres/app/xrs/serialization/TlvUtils.java | 77 + .../io/xeres/app/xrs/service/RsService.java | 160 + .../xrs/service/RsServiceInitPriority.java | 48 + .../app/xrs/service/RsServiceRegistry.java | 50 + .../xeres/app/xrs/service/RsServiceType.java | 119 + .../xeres/app/xrs/service/chat/ChatFlags.java | 38 + .../xeres/app/xrs/service/chat/ChatRoom.java | 244 + .../app/xrs/service/chat/ChatService.java | 838 + .../app/xrs/service/chat/MessageCache.java | 137 + .../xeres/app/xrs/service/chat/RoomFlags.java | 29 + .../xrs/service/chat/item/ChatAvatarItem.java | 55 + .../service/chat/item/ChatMessageItem.java | 98 + .../xrs/service/chat/item/ChatRoomBounce.java | 126 + .../service/chat/item/ChatRoomConfigItem.java | 49 + .../item/ChatRoomConnectChallengeItem.java | 61 + .../xrs/service/chat/item/ChatRoomEvent.java | 41 + .../service/chat/item/ChatRoomEventItem.java | 99 + .../service/chat/item/ChatRoomInviteItem.java | 111 + .../service/chat/item/ChatRoomListItem.java | 62 + .../chat/item/ChatRoomListRequestItem.java | 40 + .../chat/item/ChatRoomMessageItem.java | 102 + .../chat/item/ChatRoomUnsubscribeItem.java | 51 + .../xrs/service/chat/item/ChatStatusItem.java | 74 + .../item/PrivateChatMessageConfigItem.java | 84 + .../chat/item/PrivateOutgoingMapItem.java | 43 + .../item/SubscribedChatRoomConfigItem.java | 96 + .../chat/item/VisibleChatRoomInfo.java | 96 + .../service/discovery/DiscoveryService.java | 467 + .../discovery/item/DiscoveryContactItem.java | 421 + .../item/DiscoveryIdentityListItem.java | 58 + .../discovery/item/DiscoveryPgpKeyItem.java | 61 + .../discovery/item/DiscoveryPgpListItem.java | 94 + .../xeres/app/xrs/service/gxs/GxsService.java | 233 + .../service/gxs/GxsTransactionManager.java | 213 + .../app/xrs/service/gxs/Transaction.java | 132 + .../app/xrs/service/gxs/item/GxsExchange.java | 54 + .../service/gxs/item/GxsSyncGroupItem.java | 82 + .../gxs/item/GxsSyncGroupRequestItem.java | 86 + .../gxs/item/GxsSyncGroupStatsItem.java | 57 + .../service/gxs/item/GxsTransactionItem.java | 93 + .../gxs/item/GxsTransferGroupItem.java | 105 + .../app/xrs/service/gxs/item/RequestType.java | 27 + .../app/xrs/service/gxs/item/SyncFlags.java | 26 + .../service/gxs/item/TransactionFlags.java | 69 + .../app/xrs/service/gxsid/GxsIdService.java | 234 + .../service/gxsid/item/GxsIdGroupItem.java | 160 + .../gxsid/item/GxsIdLocalInfoItem.java | 33 + .../service/heartbeat/HeartbeatService.java | 77 + .../service/heartbeat/item/HeartbeatItem.java | 31 + .../xeres/app/xrs/service/rtt/RttService.java | 127 + .../app/xrs/service/rtt/item/RttPingItem.java | 62 + .../app/xrs/service/rtt/item/RttPongItem.java | 66 + .../serviceinfo/ServiceInfoService.java | 137 + .../service/serviceinfo/item/ServiceInfo.java | 123 + .../serviceinfo/item/ServiceListItem.java | 61 + .../service/sliceprobe/SliceProbeService.java | 71 + .../sliceprobe/item/SliceProbeItem.java | 31 + .../app/xrs/service/status/StatusService.java | 86 + .../xrs/service/status/item/StatusItem.java | 75 + app/src/main/resources/LICENSE | 674 + ...itional-spring-configuration-metadata.json | 44 + .../resources/application-cloud.properties | 20 + .../main/resources/application-dev.properties | 65 + app/src/main/resources/application.properties | 52 + app/src/main/resources/banner.txt | 8 + app/src/main/resources/bdboot.txt | 3 + .../V00_0_1_202001232214__InitDb.sql | 130 + app/src/main/resources/public/index.html | 34 + .../io/xeres/app/XeresApplicationTest.java | 34 + .../application/SingleInstanceRunTest.java | 32 + .../crypto/chatcipher/ChatChallengeTest.java | 46 + .../java/io/xeres/app/crypto/pgp/PGPTest.java | 148 + .../java/io/xeres/app/crypto/rsa/RSATest.java | 104 + .../app/crypto/rsid/RSCertificateTest.java | 61 + .../xeres/app/crypto/rsid/RSIdArmorTest.java | 32 + .../io/xeres/app/crypto/rsid/RSIdCrcTest.java | 42 + .../app/crypto/rsid/RSSerialVersionTest.java | 53 + .../app/crypto/rsid/RSShortInviteTest.java | 45 + .../rsid/certificate/RSCertificateFakes.java | 36 + .../shortinvite/ShortInviteQuirksTest.java | 53 + .../rsid/shortinvite/ShortInviteTagsTest.java | 49 + .../crypto/scramble/ScrambledStringTest.java | 102 + .../io/xeres/app/crypto/x509/X509Test.java | 105 + .../model/chatroom/ChatRoomFakes.java | 50 + .../model/connection/ConnectionFakes.java | 43 + .../connection/ConnectionMapperTest.java | 67 + .../database/model/gxs/GxsCircleTypeTest.java | 40 + .../model/gxs/GxsIdGroupItemFakes.java | 52 + .../model/gxs/GxsPrivacyFlagsTest.java | 37 + .../model/gxs/GxsSignatureFlagsTest.java | 37 + .../model/identity/IdentityFakes.java | 49 + .../model/location/LocationFakes.java | 75 + .../model/location/LocationMapperTest.java | 87 + .../app/database/model/prefs/PrefsFakes.java | 47 + .../database/model/profile/ProfileFakes.java | 69 + .../model/profile/ProfileMapperTest.java | 90 + .../repository/ChatRoomRepositoryTest.java | 81 + .../repository/GxsIdRepositoryTest.java | 76 + .../repository/LocationRepositoryTest.java | 77 + .../repository/PrefsRepositoryTest.java | 76 + .../repository/ProfileRepositoryTest.java | 72 + .../io/xeres/app/environment/CloudTest.java | 33 + .../app/environment/CommandArgumentTest.java | 33 + .../app/environment/HostVariableTest.java | 33 + .../bdisc/BroadcastDiscoveryServiceTest.java | 70 + .../net/bdisc/UdpDiscoveryProtocolTest.java | 90 + .../app/net/peer/AbstractPipelineTest.java | 35 + .../io/xeres/app/net/peer/PacketBuilder.java | 267 + .../net/peer/PacketDecoderPipelineTest.java | 390 + .../net/peer/PacketEncoderPipelineTest.java | 215 + .../xeres/app/net/peer/PeerAttributeTest.java | 32 + .../net/peer/RawItemDecoderPipelineTest.java | 86 + .../net/peer/packet/MultiPacketBuilder.java | 107 + .../xeres/app/net/peer/packet/PacketTest.java | 42 + .../net/peer/packet/SimplePacketBuilder.java | 116 + .../io/xeres/app/net/peer/ssl/SSLTest.java | 159 + .../app/net/protocol/PeerAddressTest.java | 307 + .../net/protocol/tor/OnionAddressTest.java | 48 + .../xeres/app/net/upnp/ControlPointTest.java | 97 + .../io/xeres/app/net/upnp/DeviceTest.java | 87 + .../xeres/app/net/upnp/PortMappingTest.java | 57 + .../java/io/xeres/app/net/upnp/SoapTest.java | 117 + .../xeres/app/net/upnp/UPNPServiceTest.java | 50 + .../app/service/IdentityServiceTest.java | 153 + .../app/service/LocationServiceTest.java | 209 + .../xeres/app/service/PrefsServiceTest.java | 59 + .../xeres/app/service/ProfileServiceTest.java | 145 + .../controller/AbstractControllerTest.java | 76 + .../web/api/controller/PathConfigTest.java | 33 + .../config/ConfigControllerTest.java | 281 + .../profile/ProfileControllerTest.java | 197 + .../xeres/app/xrs/item/ItemFactoryTest.java | 32 + .../xeres/app/xrs/item/ItemPriorityTest.java | 36 + .../app/xrs/serialization/SerialAll.java | 328 + .../app/xrs/serialization/SerialEnum.java | 28 + .../app/xrs/serialization/SerialList.java | 33 + .../app/xrs/serialization/SerialMap.java | 33 + .../app/xrs/serialization/SerializerTest.java | 573 + .../app/xrs/serialization/TlvTypeTest.java | 54 + .../app/xrs/serialization/TlvUtilsTest.java | 32 + .../xrs/service/RsServiceRegistryTest.java | 32 + .../app/xrs/service/chat/ChatFlagsTest.java | 47 + .../xrs/service/chat/ChatRoomEventTest.java | 38 + .../app/xrs/service/chat/ChatServiceTest.java | 63 + .../app/xrs/service/chat/RoomFlagsTest.java | 38 + .../discovery/DiscoveryPgpListItemTest.java | 36 + .../discovery/DiscoveryServiceTest.java | 250 + .../xrs/service/gxs/GxsRequestTypeTest.java | 36 + .../app/xrs/service/gxs/GxsSignatureTest.java | 80 + .../app/xrs/service/gxs/SyncFlagsTest.java | 36 + .../xrs/service/gxs/TransactionFlagsTest.java | 47 + .../xrs/service/heartbeat/HeartbeatTest.java | 45 + .../app/xrs/service/rtt/RttServiceTest.java | 66 + .../app/xrs/service/status/StatusTest.java | 38 + .../io/xeres/testutils/FakeHTTPServer.java | 63 + .../java/io/xeres/testutils/TestUtils.java | 45 + .../resources/application-default.properties | 19 + .../test/resources/upnp/routers/RT-AC87U.xml | 77 + build.gradle | 73 + common/.gitignore | 1 + common/build.gradle | 37 + .../main/java/io/xeres/common/AppName.java | 30 + .../common/dto/connection/ConnectionDTO.java | 32 + .../dto/identity/IdentityConstants.java | 33 + .../dto/location/LocationConstants.java | 33 + .../common/dto/location/LocationDTO.java | 64 + .../common/dto/profile/ProfileConstants.java | 33 + .../xeres/common/dto/profile/ProfileDTO.java | 68 + .../main/java/io/xeres/common/id/GxsId.java | 82 + .../src/main/java/io/xeres/common/id/Id.java | 204 + .../java/io/xeres/common/id/Identifier.java | 58 + .../java/io/xeres/common/id/LocationId.java | 97 + .../xeres/common/id/ProfileFingerprint.java | 93 + .../main/java/io/xeres/common/id/Sha1Sum.java | 82 + .../java/io/xeres/common/identity/Type.java | 40 + .../xeres/common/message/MessageHeaders.java | 50 + .../io/xeres/common/message/MessageType.java | 32 + .../common/message/chat/ChatConstants.java | 32 + .../common/message/chat/ChatMessage.java | 62 + .../message/chat/ChatRoomListMessage.java | 38 + .../common/message/chat/ChatRoomMessage.java | 83 + .../message/chat/PrivateChatMessage.java | 58 + .../xeres/common/message/chat/RoomInfo.java | 133 + .../xeres/common/message/chat/RoomType.java | 34 + .../main/java/io/xeres/common/pgp/Trust.java | 66 + .../common/properties/StartupProperties.java | 174 + .../io/xeres/common/protocol/NetMode.java | 42 + .../java/io/xeres/common/protocol/ip/IP.java | 346 + .../common/protocol/ip/package-info.java | 23 + .../java/io/xeres/common/rest/PathConfig.java | 36 + .../rest/chat/CreateChatRoomRequest.java | 32 + .../common/rest/config/HostnameResponse.java | 24 + .../common/rest/config/IpAddressRequest.java | 43 + .../common/rest/config/IpAddressResponse.java | 24 + .../rest/config/OwnIdentityRequest.java | 35 + .../rest/config/OwnLocationRequest.java | 33 + .../common/rest/config/OwnProfileRequest.java | 33 + .../common/rest/config/UsernameResponse.java | 24 + .../common/rest/location/RSIdResponse.java | 24 + .../rest/profile/CertificateRequest.java | 33 + .../common/util/NoSuppressedRunnable.java | 50 + .../test/java/io/xeres/common/id/IdTest.java | 122 + .../io/xeres/common/identity/TypeTest.java | 36 + .../java/io/xeres/common/pgp/TrustTest.java | 38 + .../io/xeres/common/protocol/ip/IPTest.java | 102 + .../xeres/common/protocol/ip/NetModeTest.java | 39 + doc/manual.adoc | 71 + doc/services/chat.adoc | 77 + doc/services/gxsid.adoc | 5 + doc/services/gxstrans.adoc | 39 + doc/services/heartbeat.adoc | 21 + doc/services/rtt.adoc | 59 + docker-compose.yml | 13 + gradle/wrapper/gradle-wrapper.jar | Bin 0 -> 59536 bytes gradle/wrapper/gradle-wrapper.properties | 5 + gradlew | 185 + gradlew.bat | 89 + icon.ico | Bin 0 -> 9062 bytes installer/startup.jpg | Bin 0 -> 8898 bytes settings.gradle | 25 + ui/.gitignore | 2 + ui/build.gradle | 39 + .../java/io/xeres/ui/JavaFxApplication.java | 99 + .../io/xeres/ui/PrimaryStageInitializer.java | 73 + .../java/io/xeres/ui/StageReadyEvent.java | 39 + ui/src/main/java/io/xeres/ui/UiStarter.java | 33 + .../java/io/xeres/ui/client/ChatClient.java | 82 + .../java/io/xeres/ui/client/ConfigClient.java | 111 + .../io/xeres/ui/client/ConnectionClient.java | 62 + .../io/xeres/ui/client/LocationClient.java | 72 + .../io/xeres/ui/client/ProfileClient.java | 115 + .../ui/client/message/ChatFrameHandler.java | 89 + .../ui/client/message/MessageClient.java | 151 + .../client/message/PendingSubscription.java | 44 + .../ui/client/message/SessionHandler.java | 45 + .../io/xeres/ui/controller/Controller.java | 30 + .../ui/controller/MainWindowController.java | 143 + .../xeres/ui/controller/WindowController.java | 51 + .../about/AboutWindowController.java | 56 + .../AccountCreationWindowController.java | 177 + .../ui/controller/chat/ChatListView.java | 81 + .../ChatRoomCreationWindowController.java | 66 + .../chat/ChatRoomInfoController.java | 66 + .../controller/chat/ChatViewController.java | 319 + .../xeres/ui/controller/chat/RoomHolder.java | 75 + .../id/AddCertificateWindowController.java | 153 + .../messaging/BroadcastWindowController.java | 65 + .../messaging/FriendsWindowController.java | 123 + .../messaging/MessagingWindowController.java | 161 + .../controller/messaging/ProfileHolder.java | 65 + .../profile/ProfilesUiController.java | 78 + .../java/io/xeres/ui/custom/ChatListCell.java | 39 + .../xeres/ui/custom/NullSelectionModel.java | 120 + .../xeres/ui/model/connection/Connection.java | 70 + .../ui/model/connection/ConnectionMapper.java | 45 + .../io/xeres/ui/model/location/Location.java | 103 + .../ui/model/location/LocationMapper.java | 65 + .../io/xeres/ui/model/profile/Profile.java | 114 + .../xeres/ui/model/profile/ProfileMapper.java | 66 + .../io/xeres/ui/support/tray/TrayService.java | 159 + .../io/xeres/ui/support/util/UiUtils.java | 190 + .../io/xeres/ui/support/window/UiWindow.java | 220 + .../ui/support/window/WindowManager.java | 184 + ui/src/main/resources/image/icon.png | Bin 0 -> 17701 bytes ui/src/main/resources/image/trayicon.png | Bin 0 -> 1204 bytes ui/src/main/resources/view/about/about.fxml | 50 + .../view/account/account_creation.fxml | 61 + .../resources/view/chat/chat_roominfo.fxml | 55 + .../resources/view/chat/chatroom_create.fxml | 41 + ui/src/main/resources/view/chat/chatview.fxml | 33 + .../resources/view/id/certificate_add.fxml | 99 + ui/src/main/resources/view/javafx.css | 52 + ui/src/main/resources/view/main.fxml | 68 + .../resources/view/messaging/broadcast.fxml | 50 + .../resources/view/messaging/friends.fxml | 43 + .../resources/view/messaging/messaging.fxml | 42 + .../main/resources/view/profile/profiles.fxml | 38 + webui/.gitignore | 2 + webui/build.gradle | 78 + webui/src/main/webapp/.browserslistrc | 17 + webui/src/main/webapp/.gitignore | 46 + webui/src/main/webapp/README.md | 27 + webui/src/main/webapp/angular.json | 128 + webui/src/main/webapp/e2e/protractor.conf.js | 40 + webui/src/main/webapp/e2e/src/app.e2e-spec.ts | 23 + webui/src/main/webapp/e2e/src/app.po.ts | 11 + webui/src/main/webapp/e2e/tsconfig.json | 13 + webui/src/main/webapp/karma.conf.js | 45 + webui/src/main/webapp/package-lock.json | 13488 ++++++++++++++++ webui/src/main/webapp/package.json | 48 + .../account-creation.component.css | 19 + .../account-creation.component.html | 20 + .../account-creation.component.spec.ts | 44 + .../account-creation.component.ts | 35 + .../account-creation.module.ts | 36 + .../account-creation/configclient.service.ts | 49 + .../main/webapp/src/app/app-routing.module.ts | 11 + .../src/main/webapp/src/app/app.component.css | 0 .../main/webapp/src/app/app.component.html | 32 + .../main/webapp/src/app/app.component.spec.ts | 35 + .../src/main/webapp/src/app/app.component.ts | 29 + webui/src/main/webapp/src/app/app.module.ts | 22 + .../webapp/src/app/shared/shared.module.ts | 36 + webui/src/main/webapp/src/assets/.gitkeep | 0 .../main/webapp/src/assets/images/logo.png | Bin 0 -> 1204 bytes .../src/environments/environment.prod.ts | 3 + .../webapp/src/environments/environment.ts | 16 + webui/src/main/webapp/src/favicon.ico | Bin 0 -> 948 bytes webui/src/main/webapp/src/index.html | 13 + webui/src/main/webapp/src/main.ts | 12 + webui/src/main/webapp/src/polyfills.ts | 63 + webui/src/main/webapp/src/styles.css | 22 + webui/src/main/webapp/src/test.ts | 22 + webui/src/main/webapp/tsconfig.app.json | 15 + webui/src/main/webapp/tsconfig.json | 29 + webui/src/main/webapp/tsconfig.spec.json | 18 + webui/src/main/webapp/tslint.json | 152 + 486 files changed, 57467 insertions(+) create mode 100644 .editorconfig create mode 100644 .gitignore create mode 100644 .jpb/persistence-units.xml create mode 100644 LICENSE create mode 100644 README.md create mode 100644 app/.gitignore create mode 100644 app/build.gradle create mode 100644 app/src/main/java/io/xeres/app/XeresApplication.java create mode 100644 app/src/main/java/io/xeres/app/application/SingleInstanceRun.java create mode 100644 app/src/main/java/io/xeres/app/application/Startup.java create mode 100644 app/src/main/java/io/xeres/app/application/environment/Cloud.java create mode 100644 app/src/main/java/io/xeres/app/application/environment/CommandArgument.java create mode 100644 app/src/main/java/io/xeres/app/application/environment/HostVariable.java create mode 100644 app/src/main/java/io/xeres/app/application/events/LocationReadyEvent.java create mode 100644 app/src/main/java/io/xeres/app/application/events/package-info.java create mode 100644 app/src/main/java/io/xeres/app/configuration/DataDirConfiguration.java create mode 100644 app/src/main/java/io/xeres/app/configuration/DataSourceConfiguration.java create mode 100644 app/src/main/java/io/xeres/app/configuration/SchedulerConfiguration.java create mode 100644 app/src/main/java/io/xeres/app/configuration/WebSocketConfiguration.java create mode 100644 app/src/main/java/io/xeres/app/crypto/chatcipher/ChatChallenge.java create mode 100644 app/src/main/java/io/xeres/app/crypto/pgp/PGP.java create mode 100644 app/src/main/java/io/xeres/app/crypto/pgp/PGPSigner.java create mode 100644 app/src/main/java/io/xeres/app/crypto/pgp/package-info.java create mode 100644 app/src/main/java/io/xeres/app/crypto/rsa/RSA.java create mode 100644 app/src/main/java/io/xeres/app/crypto/rsid/RSId.java create mode 100644 app/src/main/java/io/xeres/app/crypto/rsid/RSIdArmor.java create mode 100644 app/src/main/java/io/xeres/app/crypto/rsid/RSIdCrc.java create mode 100644 app/src/main/java/io/xeres/app/crypto/rsid/RSSerialVersion.java create mode 100644 app/src/main/java/io/xeres/app/crypto/rsid/certificate/RSCertificate.java create mode 100644 app/src/main/java/io/xeres/app/crypto/rsid/certificate/RSCertificateTags.java create mode 100644 app/src/main/java/io/xeres/app/crypto/rsid/shortinvite/ShortInvite.java create mode 100644 app/src/main/java/io/xeres/app/crypto/rsid/shortinvite/ShortInviteQuirks.java create mode 100644 app/src/main/java/io/xeres/app/crypto/rsid/shortinvite/ShortInviteTags.java create mode 100644 app/src/main/java/io/xeres/app/crypto/scramble/ScrambledString.java create mode 100644 app/src/main/java/io/xeres/app/crypto/x509/X509.java create mode 100644 app/src/main/java/io/xeres/app/database/DatabaseSession.java create mode 100644 app/src/main/java/io/xeres/app/database/DatabaseSessionManager.java create mode 100644 app/src/main/java/io/xeres/app/database/converter/EnumSetConverter.java create mode 100644 app/src/main/java/io/xeres/app/database/converter/GxsPrivacyFlagsConverter.java create mode 100644 app/src/main/java/io/xeres/app/database/converter/GxsSignatureFlagsConverter.java create mode 100644 app/src/main/java/io/xeres/app/database/model/chatroom/ChatRoom.java create mode 100644 app/src/main/java/io/xeres/app/database/model/connection/Connection.java create mode 100644 app/src/main/java/io/xeres/app/database/model/connection/ConnectionMapper.java create mode 100644 app/src/main/java/io/xeres/app/database/model/gxs/GxsCircleType.java create mode 100644 app/src/main/java/io/xeres/app/database/model/gxs/GxsClientUpdate.java create mode 100644 app/src/main/java/io/xeres/app/database/model/gxs/GxsGroupItem.java create mode 100644 app/src/main/java/io/xeres/app/database/model/gxs/GxsMessageItem.java create mode 100644 app/src/main/java/io/xeres/app/database/model/gxs/GxsPrivacyFlags.java create mode 100644 app/src/main/java/io/xeres/app/database/model/gxs/GxsServiceSetting.java create mode 100644 app/src/main/java/io/xeres/app/database/model/gxs/GxsSignatureFlags.java create mode 100644 app/src/main/java/io/xeres/app/database/model/identity/Identity.java create mode 100644 app/src/main/java/io/xeres/app/database/model/location/Location.java create mode 100644 app/src/main/java/io/xeres/app/database/model/location/LocationMapper.java create mode 100644 app/src/main/java/io/xeres/app/database/model/prefs/Prefs.java create mode 100644 app/src/main/java/io/xeres/app/database/model/profile/Profile.java create mode 100644 app/src/main/java/io/xeres/app/database/model/profile/ProfileMapper.java create mode 100644 app/src/main/java/io/xeres/app/database/repository/ChatRoomRepository.java create mode 100644 app/src/main/java/io/xeres/app/database/repository/GxsClientUpdateRepository.java create mode 100644 app/src/main/java/io/xeres/app/database/repository/GxsIdRepository.java create mode 100644 app/src/main/java/io/xeres/app/database/repository/GxsServiceSettingRepository.java create mode 100644 app/src/main/java/io/xeres/app/database/repository/IdentityRepository.java create mode 100644 app/src/main/java/io/xeres/app/database/repository/LocationRepository.java create mode 100644 app/src/main/java/io/xeres/app/database/repository/PrefsRepository.java create mode 100644 app/src/main/java/io/xeres/app/database/repository/ProfileRepository.java create mode 100644 app/src/main/java/io/xeres/app/job/PeerConnectionJob.java create mode 100644 app/src/main/java/io/xeres/app/net/bdisc/BroadcastDiscoveryService.java create mode 100644 app/src/main/java/io/xeres/app/net/bdisc/UdpDiscoveryPeer.java create mode 100644 app/src/main/java/io/xeres/app/net/bdisc/UdpDiscoveryProtocol.java create mode 100644 app/src/main/java/io/xeres/app/net/dht/DHTService.java create mode 100644 app/src/main/java/io/xeres/app/net/dht/DHTSpringLog.java create mode 100644 app/src/main/java/io/xeres/app/net/dht/package-info.java create mode 100644 app/src/main/java/io/xeres/app/net/peer/ConnectionDirection.java create mode 100644 app/src/main/java/io/xeres/app/net/peer/PeerAttribute.java create mode 100644 app/src/main/java/io/xeres/app/net/peer/PeerConnection.java create mode 100644 app/src/main/java/io/xeres/app/net/peer/PeerConnectionManager.java create mode 100644 app/src/main/java/io/xeres/app/net/peer/bootstrap/PeerClient.java create mode 100644 app/src/main/java/io/xeres/app/net/peer/bootstrap/PeerInitializer.java create mode 100644 app/src/main/java/io/xeres/app/net/peer/bootstrap/PeerServer.java create mode 100644 app/src/main/java/io/xeres/app/net/peer/packet/MultiPacket.java create mode 100644 app/src/main/java/io/xeres/app/net/peer/packet/Packet.java create mode 100644 app/src/main/java/io/xeres/app/net/peer/packet/SimplePacket.java create mode 100644 app/src/main/java/io/xeres/app/net/peer/packet/package-info.java create mode 100644 app/src/main/java/io/xeres/app/net/peer/pipeline/IdleEventHandler.java create mode 100644 app/src/main/java/io/xeres/app/net/peer/pipeline/ItemDecoder.java create mode 100644 app/src/main/java/io/xeres/app/net/peer/pipeline/ItemEncoder.java create mode 100644 app/src/main/java/io/xeres/app/net/peer/pipeline/MultiPacketEncoder.java create mode 100644 app/src/main/java/io/xeres/app/net/peer/pipeline/PacketDecoder.java create mode 100644 app/src/main/java/io/xeres/app/net/peer/pipeline/PeerHandler.java create mode 100644 app/src/main/java/io/xeres/app/net/peer/pipeline/SimplePacketEncoder.java create mode 100644 app/src/main/java/io/xeres/app/net/peer/pipeline/package-info.java create mode 100644 app/src/main/java/io/xeres/app/net/peer/ssl/SSL.java create mode 100644 app/src/main/java/io/xeres/app/net/protocol/PeerAddress.java create mode 100644 app/src/main/java/io/xeres/app/net/protocol/tor/OnionAddress.java create mode 100644 app/src/main/java/io/xeres/app/net/protocol/tor/package-info.java create mode 100644 app/src/main/java/io/xeres/app/net/tou/package-info.java create mode 100644 app/src/main/java/io/xeres/app/net/upnp/ControlPoint.java create mode 100644 app/src/main/java/io/xeres/app/net/upnp/Device.java create mode 100644 app/src/main/java/io/xeres/app/net/upnp/DeviceSpecs.java create mode 100644 app/src/main/java/io/xeres/app/net/upnp/HttpuHeader.java create mode 100644 app/src/main/java/io/xeres/app/net/upnp/PortMapping.java create mode 100644 app/src/main/java/io/xeres/app/net/upnp/Protocol.java create mode 100644 app/src/main/java/io/xeres/app/net/upnp/Soap.java create mode 100644 app/src/main/java/io/xeres/app/net/upnp/UPNPService.java create mode 100644 app/src/main/java/io/xeres/app/net/upnp/package-info.java create mode 100644 app/src/main/java/io/xeres/app/properties/DatabaseProperties.java create mode 100644 app/src/main/java/io/xeres/app/properties/NetworkProperties.java create mode 100644 app/src/main/java/io/xeres/app/properties/UiProperties.java create mode 100644 app/src/main/java/io/xeres/app/service/ChatRoomService.java create mode 100644 app/src/main/java/io/xeres/app/service/GxsExchangeService.java create mode 100644 app/src/main/java/io/xeres/app/service/IdentityService.java create mode 100644 app/src/main/java/io/xeres/app/service/LocationService.java create mode 100644 app/src/main/java/io/xeres/app/service/PeerService.java create mode 100644 app/src/main/java/io/xeres/app/service/PrefsService.java create mode 100644 app/src/main/java/io/xeres/app/service/ProfileService.java create mode 100644 app/src/main/java/io/xeres/app/web/api/DefaultHandler.java create mode 100644 app/src/main/java/io/xeres/app/web/api/controller/chat/ChatController.java create mode 100644 app/src/main/java/io/xeres/app/web/api/controller/chat/ChatMessageController.java create mode 100644 app/src/main/java/io/xeres/app/web/api/controller/config/ConfigController.java create mode 100644 app/src/main/java/io/xeres/app/web/api/controller/connection/ConnectionController.java create mode 100644 app/src/main/java/io/xeres/app/web/api/controller/location/LocationController.java create mode 100644 app/src/main/java/io/xeres/app/web/api/controller/notification/NotificationController.java create mode 100644 app/src/main/java/io/xeres/app/web/api/controller/profile/ProfileController.java create mode 100644 app/src/main/java/io/xeres/app/web/api/error/Error.java create mode 100644 app/src/main/java/io/xeres/app/web/api/error/ErrorResponseEntity.java create mode 100644 app/src/main/java/io/xeres/app/web/api/error/exception/InternalServerErrorException.java create mode 100644 app/src/main/java/io/xeres/app/web/api/error/exception/UnprocessableEntityException.java create mode 100644 app/src/main/java/io/xeres/app/web/api/sse/SsePushNotificationService.java create mode 100644 app/src/main/java/io/xeres/app/xrs/common/SecurityKey.java create mode 100644 app/src/main/java/io/xeres/app/xrs/common/SecurityKeySet.java create mode 100644 app/src/main/java/io/xeres/app/xrs/common/Signature.java create mode 100644 app/src/main/java/io/xeres/app/xrs/common/SignatureSet.java create mode 100644 app/src/main/java/io/xeres/app/xrs/item/Item.java create mode 100644 app/src/main/java/io/xeres/app/xrs/item/ItemFactory.java create mode 100644 app/src/main/java/io/xeres/app/xrs/item/ItemPriority.java create mode 100644 app/src/main/java/io/xeres/app/xrs/item/RawItem.java create mode 100644 app/src/main/java/io/xeres/app/xrs/serialization/AnnotationSerializer.java create mode 100644 app/src/main/java/io/xeres/app/xrs/serialization/ArraySerializer.java create mode 100644 app/src/main/java/io/xeres/app/xrs/serialization/BooleanSerializer.java create mode 100644 app/src/main/java/io/xeres/app/xrs/serialization/ByteArraySerializer.java create mode 100644 app/src/main/java/io/xeres/app/xrs/serialization/ByteSerializer.java create mode 100644 app/src/main/java/io/xeres/app/xrs/serialization/DoubleSerializer.java create mode 100644 app/src/main/java/io/xeres/app/xrs/serialization/EnumSerializer.java create mode 100644 app/src/main/java/io/xeres/app/xrs/serialization/EnumSetSerializer.java create mode 100644 app/src/main/java/io/xeres/app/xrs/serialization/FieldSize.java create mode 100644 app/src/main/java/io/xeres/app/xrs/serialization/FloatSerializer.java create mode 100644 app/src/main/java/io/xeres/app/xrs/serialization/IdentifierSerializer.java create mode 100644 app/src/main/java/io/xeres/app/xrs/serialization/IntSerializer.java create mode 100644 app/src/main/java/io/xeres/app/xrs/serialization/ListSerializer.java create mode 100644 app/src/main/java/io/xeres/app/xrs/serialization/LongSerializer.java create mode 100644 app/src/main/java/io/xeres/app/xrs/serialization/MapSerializer.java create mode 100644 app/src/main/java/io/xeres/app/xrs/serialization/RsSerializable.java create mode 100644 app/src/main/java/io/xeres/app/xrs/serialization/RsSerializableSerializer.java create mode 100644 app/src/main/java/io/xeres/app/xrs/serialization/RsSerialized.java create mode 100644 app/src/main/java/io/xeres/app/xrs/serialization/SerializationFlags.java create mode 100644 app/src/main/java/io/xeres/app/xrs/serialization/Serializer.java create mode 100644 app/src/main/java/io/xeres/app/xrs/serialization/ShortSerializer.java create mode 100644 app/src/main/java/io/xeres/app/xrs/serialization/StringSerializer.java create mode 100644 app/src/main/java/io/xeres/app/xrs/serialization/TlvAddressSerializer.java create mode 100644 app/src/main/java/io/xeres/app/xrs/serialization/TlvBinarySerializer.java create mode 100644 app/src/main/java/io/xeres/app/xrs/serialization/TlvSecurityKeySerializer.java create mode 100644 app/src/main/java/io/xeres/app/xrs/serialization/TlvSecurityKeySetSerializer.java create mode 100644 app/src/main/java/io/xeres/app/xrs/serialization/TlvSerializer.java create mode 100644 app/src/main/java/io/xeres/app/xrs/serialization/TlvSetSerializer.java create mode 100644 app/src/main/java/io/xeres/app/xrs/serialization/TlvSignatureSerializer.java create mode 100644 app/src/main/java/io/xeres/app/xrs/serialization/TlvSignatureSetSerializer.java create mode 100644 app/src/main/java/io/xeres/app/xrs/serialization/TlvStringSerializer.java create mode 100644 app/src/main/java/io/xeres/app/xrs/serialization/TlvStringSetRefSerializer.java create mode 100644 app/src/main/java/io/xeres/app/xrs/serialization/TlvType.java create mode 100644 app/src/main/java/io/xeres/app/xrs/serialization/TlvUint32Serializer.java create mode 100644 app/src/main/java/io/xeres/app/xrs/serialization/TlvUtils.java create mode 100644 app/src/main/java/io/xeres/app/xrs/service/RsService.java create mode 100644 app/src/main/java/io/xeres/app/xrs/service/RsServiceInitPriority.java create mode 100644 app/src/main/java/io/xeres/app/xrs/service/RsServiceRegistry.java create mode 100644 app/src/main/java/io/xeres/app/xrs/service/RsServiceType.java create mode 100644 app/src/main/java/io/xeres/app/xrs/service/chat/ChatFlags.java create mode 100644 app/src/main/java/io/xeres/app/xrs/service/chat/ChatRoom.java create mode 100644 app/src/main/java/io/xeres/app/xrs/service/chat/ChatService.java create mode 100644 app/src/main/java/io/xeres/app/xrs/service/chat/MessageCache.java create mode 100644 app/src/main/java/io/xeres/app/xrs/service/chat/RoomFlags.java create mode 100644 app/src/main/java/io/xeres/app/xrs/service/chat/item/ChatAvatarItem.java create mode 100644 app/src/main/java/io/xeres/app/xrs/service/chat/item/ChatMessageItem.java create mode 100644 app/src/main/java/io/xeres/app/xrs/service/chat/item/ChatRoomBounce.java create mode 100644 app/src/main/java/io/xeres/app/xrs/service/chat/item/ChatRoomConfigItem.java create mode 100644 app/src/main/java/io/xeres/app/xrs/service/chat/item/ChatRoomConnectChallengeItem.java create mode 100644 app/src/main/java/io/xeres/app/xrs/service/chat/item/ChatRoomEvent.java create mode 100644 app/src/main/java/io/xeres/app/xrs/service/chat/item/ChatRoomEventItem.java create mode 100644 app/src/main/java/io/xeres/app/xrs/service/chat/item/ChatRoomInviteItem.java create mode 100644 app/src/main/java/io/xeres/app/xrs/service/chat/item/ChatRoomListItem.java create mode 100644 app/src/main/java/io/xeres/app/xrs/service/chat/item/ChatRoomListRequestItem.java create mode 100644 app/src/main/java/io/xeres/app/xrs/service/chat/item/ChatRoomMessageItem.java create mode 100644 app/src/main/java/io/xeres/app/xrs/service/chat/item/ChatRoomUnsubscribeItem.java create mode 100644 app/src/main/java/io/xeres/app/xrs/service/chat/item/ChatStatusItem.java create mode 100644 app/src/main/java/io/xeres/app/xrs/service/chat/item/PrivateChatMessageConfigItem.java create mode 100644 app/src/main/java/io/xeres/app/xrs/service/chat/item/PrivateOutgoingMapItem.java create mode 100644 app/src/main/java/io/xeres/app/xrs/service/chat/item/SubscribedChatRoomConfigItem.java create mode 100644 app/src/main/java/io/xeres/app/xrs/service/chat/item/VisibleChatRoomInfo.java create mode 100644 app/src/main/java/io/xeres/app/xrs/service/discovery/DiscoveryService.java create mode 100644 app/src/main/java/io/xeres/app/xrs/service/discovery/item/DiscoveryContactItem.java create mode 100644 app/src/main/java/io/xeres/app/xrs/service/discovery/item/DiscoveryIdentityListItem.java create mode 100644 app/src/main/java/io/xeres/app/xrs/service/discovery/item/DiscoveryPgpKeyItem.java create mode 100644 app/src/main/java/io/xeres/app/xrs/service/discovery/item/DiscoveryPgpListItem.java create mode 100644 app/src/main/java/io/xeres/app/xrs/service/gxs/GxsService.java create mode 100644 app/src/main/java/io/xeres/app/xrs/service/gxs/GxsTransactionManager.java create mode 100644 app/src/main/java/io/xeres/app/xrs/service/gxs/Transaction.java create mode 100644 app/src/main/java/io/xeres/app/xrs/service/gxs/item/GxsExchange.java create mode 100644 app/src/main/java/io/xeres/app/xrs/service/gxs/item/GxsSyncGroupItem.java create mode 100644 app/src/main/java/io/xeres/app/xrs/service/gxs/item/GxsSyncGroupRequestItem.java create mode 100644 app/src/main/java/io/xeres/app/xrs/service/gxs/item/GxsSyncGroupStatsItem.java create mode 100644 app/src/main/java/io/xeres/app/xrs/service/gxs/item/GxsTransactionItem.java create mode 100644 app/src/main/java/io/xeres/app/xrs/service/gxs/item/GxsTransferGroupItem.java create mode 100644 app/src/main/java/io/xeres/app/xrs/service/gxs/item/RequestType.java create mode 100644 app/src/main/java/io/xeres/app/xrs/service/gxs/item/SyncFlags.java create mode 100644 app/src/main/java/io/xeres/app/xrs/service/gxs/item/TransactionFlags.java create mode 100644 app/src/main/java/io/xeres/app/xrs/service/gxsid/GxsIdService.java create mode 100644 app/src/main/java/io/xeres/app/xrs/service/gxsid/item/GxsIdGroupItem.java create mode 100644 app/src/main/java/io/xeres/app/xrs/service/gxsid/item/GxsIdLocalInfoItem.java create mode 100644 app/src/main/java/io/xeres/app/xrs/service/heartbeat/HeartbeatService.java create mode 100644 app/src/main/java/io/xeres/app/xrs/service/heartbeat/item/HeartbeatItem.java create mode 100644 app/src/main/java/io/xeres/app/xrs/service/rtt/RttService.java create mode 100644 app/src/main/java/io/xeres/app/xrs/service/rtt/item/RttPingItem.java create mode 100644 app/src/main/java/io/xeres/app/xrs/service/rtt/item/RttPongItem.java create mode 100644 app/src/main/java/io/xeres/app/xrs/service/serviceinfo/ServiceInfoService.java create mode 100644 app/src/main/java/io/xeres/app/xrs/service/serviceinfo/item/ServiceInfo.java create mode 100644 app/src/main/java/io/xeres/app/xrs/service/serviceinfo/item/ServiceListItem.java create mode 100644 app/src/main/java/io/xeres/app/xrs/service/sliceprobe/SliceProbeService.java create mode 100644 app/src/main/java/io/xeres/app/xrs/service/sliceprobe/item/SliceProbeItem.java create mode 100644 app/src/main/java/io/xeres/app/xrs/service/status/StatusService.java create mode 100644 app/src/main/java/io/xeres/app/xrs/service/status/item/StatusItem.java create mode 100644 app/src/main/resources/LICENSE create mode 100644 app/src/main/resources/META-INF/additional-spring-configuration-metadata.json create mode 100644 app/src/main/resources/application-cloud.properties create mode 100644 app/src/main/resources/application-dev.properties create mode 100644 app/src/main/resources/application.properties create mode 100644 app/src/main/resources/banner.txt create mode 100644 app/src/main/resources/bdboot.txt create mode 100644 app/src/main/resources/db/migration/V00_0_1_202001232214__InitDb.sql create mode 100644 app/src/main/resources/public/index.html create mode 100644 app/src/test/java/io/xeres/app/XeresApplicationTest.java create mode 100644 app/src/test/java/io/xeres/app/application/SingleInstanceRunTest.java create mode 100644 app/src/test/java/io/xeres/app/crypto/chatcipher/ChatChallengeTest.java create mode 100644 app/src/test/java/io/xeres/app/crypto/pgp/PGPTest.java create mode 100644 app/src/test/java/io/xeres/app/crypto/rsa/RSATest.java create mode 100644 app/src/test/java/io/xeres/app/crypto/rsid/RSCertificateTest.java create mode 100644 app/src/test/java/io/xeres/app/crypto/rsid/RSIdArmorTest.java create mode 100644 app/src/test/java/io/xeres/app/crypto/rsid/RSIdCrcTest.java create mode 100644 app/src/test/java/io/xeres/app/crypto/rsid/RSSerialVersionTest.java create mode 100644 app/src/test/java/io/xeres/app/crypto/rsid/RSShortInviteTest.java create mode 100644 app/src/test/java/io/xeres/app/crypto/rsid/certificate/RSCertificateFakes.java create mode 100644 app/src/test/java/io/xeres/app/crypto/rsid/shortinvite/ShortInviteQuirksTest.java create mode 100644 app/src/test/java/io/xeres/app/crypto/rsid/shortinvite/ShortInviteTagsTest.java create mode 100644 app/src/test/java/io/xeres/app/crypto/scramble/ScrambledStringTest.java create mode 100644 app/src/test/java/io/xeres/app/crypto/x509/X509Test.java create mode 100644 app/src/test/java/io/xeres/app/database/model/chatroom/ChatRoomFakes.java create mode 100644 app/src/test/java/io/xeres/app/database/model/connection/ConnectionFakes.java create mode 100644 app/src/test/java/io/xeres/app/database/model/connection/ConnectionMapperTest.java create mode 100644 app/src/test/java/io/xeres/app/database/model/gxs/GxsCircleTypeTest.java create mode 100644 app/src/test/java/io/xeres/app/database/model/gxs/GxsIdGroupItemFakes.java create mode 100644 app/src/test/java/io/xeres/app/database/model/gxs/GxsPrivacyFlagsTest.java create mode 100644 app/src/test/java/io/xeres/app/database/model/gxs/GxsSignatureFlagsTest.java create mode 100644 app/src/test/java/io/xeres/app/database/model/identity/IdentityFakes.java create mode 100644 app/src/test/java/io/xeres/app/database/model/location/LocationFakes.java create mode 100644 app/src/test/java/io/xeres/app/database/model/location/LocationMapperTest.java create mode 100644 app/src/test/java/io/xeres/app/database/model/prefs/PrefsFakes.java create mode 100644 app/src/test/java/io/xeres/app/database/model/profile/ProfileFakes.java create mode 100644 app/src/test/java/io/xeres/app/database/model/profile/ProfileMapperTest.java create mode 100644 app/src/test/java/io/xeres/app/database/repository/ChatRoomRepositoryTest.java create mode 100644 app/src/test/java/io/xeres/app/database/repository/GxsIdRepositoryTest.java create mode 100644 app/src/test/java/io/xeres/app/database/repository/LocationRepositoryTest.java create mode 100644 app/src/test/java/io/xeres/app/database/repository/PrefsRepositoryTest.java create mode 100644 app/src/test/java/io/xeres/app/database/repository/ProfileRepositoryTest.java create mode 100644 app/src/test/java/io/xeres/app/environment/CloudTest.java create mode 100644 app/src/test/java/io/xeres/app/environment/CommandArgumentTest.java create mode 100644 app/src/test/java/io/xeres/app/environment/HostVariableTest.java create mode 100644 app/src/test/java/io/xeres/app/net/bdisc/BroadcastDiscoveryServiceTest.java create mode 100644 app/src/test/java/io/xeres/app/net/bdisc/UdpDiscoveryProtocolTest.java create mode 100644 app/src/test/java/io/xeres/app/net/peer/AbstractPipelineTest.java create mode 100644 app/src/test/java/io/xeres/app/net/peer/PacketBuilder.java create mode 100644 app/src/test/java/io/xeres/app/net/peer/PacketDecoderPipelineTest.java create mode 100644 app/src/test/java/io/xeres/app/net/peer/PacketEncoderPipelineTest.java create mode 100644 app/src/test/java/io/xeres/app/net/peer/PeerAttributeTest.java create mode 100644 app/src/test/java/io/xeres/app/net/peer/RawItemDecoderPipelineTest.java create mode 100644 app/src/test/java/io/xeres/app/net/peer/packet/MultiPacketBuilder.java create mode 100644 app/src/test/java/io/xeres/app/net/peer/packet/PacketTest.java create mode 100644 app/src/test/java/io/xeres/app/net/peer/packet/SimplePacketBuilder.java create mode 100644 app/src/test/java/io/xeres/app/net/peer/ssl/SSLTest.java create mode 100644 app/src/test/java/io/xeres/app/net/protocol/PeerAddressTest.java create mode 100644 app/src/test/java/io/xeres/app/net/protocol/tor/OnionAddressTest.java create mode 100644 app/src/test/java/io/xeres/app/net/upnp/ControlPointTest.java create mode 100644 app/src/test/java/io/xeres/app/net/upnp/DeviceTest.java create mode 100644 app/src/test/java/io/xeres/app/net/upnp/PortMappingTest.java create mode 100644 app/src/test/java/io/xeres/app/net/upnp/SoapTest.java create mode 100644 app/src/test/java/io/xeres/app/net/upnp/UPNPServiceTest.java create mode 100644 app/src/test/java/io/xeres/app/service/IdentityServiceTest.java create mode 100644 app/src/test/java/io/xeres/app/service/LocationServiceTest.java create mode 100644 app/src/test/java/io/xeres/app/service/PrefsServiceTest.java create mode 100644 app/src/test/java/io/xeres/app/service/ProfileServiceTest.java create mode 100644 app/src/test/java/io/xeres/app/web/api/controller/AbstractControllerTest.java create mode 100644 app/src/test/java/io/xeres/app/web/api/controller/PathConfigTest.java create mode 100644 app/src/test/java/io/xeres/app/web/api/controller/config/ConfigControllerTest.java create mode 100644 app/src/test/java/io/xeres/app/web/api/controller/profile/ProfileControllerTest.java create mode 100644 app/src/test/java/io/xeres/app/xrs/item/ItemFactoryTest.java create mode 100644 app/src/test/java/io/xeres/app/xrs/item/ItemPriorityTest.java create mode 100644 app/src/test/java/io/xeres/app/xrs/serialization/SerialAll.java create mode 100644 app/src/test/java/io/xeres/app/xrs/serialization/SerialEnum.java create mode 100644 app/src/test/java/io/xeres/app/xrs/serialization/SerialList.java create mode 100644 app/src/test/java/io/xeres/app/xrs/serialization/SerialMap.java create mode 100644 app/src/test/java/io/xeres/app/xrs/serialization/SerializerTest.java create mode 100644 app/src/test/java/io/xeres/app/xrs/serialization/TlvTypeTest.java create mode 100644 app/src/test/java/io/xeres/app/xrs/serialization/TlvUtilsTest.java create mode 100644 app/src/test/java/io/xeres/app/xrs/service/RsServiceRegistryTest.java create mode 100644 app/src/test/java/io/xeres/app/xrs/service/chat/ChatFlagsTest.java create mode 100644 app/src/test/java/io/xeres/app/xrs/service/chat/ChatRoomEventTest.java create mode 100644 app/src/test/java/io/xeres/app/xrs/service/chat/ChatServiceTest.java create mode 100644 app/src/test/java/io/xeres/app/xrs/service/chat/RoomFlagsTest.java create mode 100644 app/src/test/java/io/xeres/app/xrs/service/discovery/DiscoveryPgpListItemTest.java create mode 100644 app/src/test/java/io/xeres/app/xrs/service/discovery/DiscoveryServiceTest.java create mode 100644 app/src/test/java/io/xeres/app/xrs/service/gxs/GxsRequestTypeTest.java create mode 100644 app/src/test/java/io/xeres/app/xrs/service/gxs/GxsSignatureTest.java create mode 100644 app/src/test/java/io/xeres/app/xrs/service/gxs/SyncFlagsTest.java create mode 100644 app/src/test/java/io/xeres/app/xrs/service/gxs/TransactionFlagsTest.java create mode 100644 app/src/test/java/io/xeres/app/xrs/service/heartbeat/HeartbeatTest.java create mode 100644 app/src/test/java/io/xeres/app/xrs/service/rtt/RttServiceTest.java create mode 100644 app/src/test/java/io/xeres/app/xrs/service/status/StatusTest.java create mode 100644 app/src/test/java/io/xeres/testutils/FakeHTTPServer.java create mode 100644 app/src/test/java/io/xeres/testutils/TestUtils.java create mode 100644 app/src/test/resources/application-default.properties create mode 100644 app/src/test/resources/upnp/routers/RT-AC87U.xml create mode 100644 build.gradle create mode 100644 common/.gitignore create mode 100644 common/build.gradle create mode 100644 common/src/main/java/io/xeres/common/AppName.java create mode 100644 common/src/main/java/io/xeres/common/dto/connection/ConnectionDTO.java create mode 100644 common/src/main/java/io/xeres/common/dto/identity/IdentityConstants.java create mode 100644 common/src/main/java/io/xeres/common/dto/location/LocationConstants.java create mode 100644 common/src/main/java/io/xeres/common/dto/location/LocationDTO.java create mode 100644 common/src/main/java/io/xeres/common/dto/profile/ProfileConstants.java create mode 100644 common/src/main/java/io/xeres/common/dto/profile/ProfileDTO.java create mode 100644 common/src/main/java/io/xeres/common/id/GxsId.java create mode 100644 common/src/main/java/io/xeres/common/id/Id.java create mode 100644 common/src/main/java/io/xeres/common/id/Identifier.java create mode 100644 common/src/main/java/io/xeres/common/id/LocationId.java create mode 100644 common/src/main/java/io/xeres/common/id/ProfileFingerprint.java create mode 100644 common/src/main/java/io/xeres/common/id/Sha1Sum.java create mode 100644 common/src/main/java/io/xeres/common/identity/Type.java create mode 100644 common/src/main/java/io/xeres/common/message/MessageHeaders.java create mode 100644 common/src/main/java/io/xeres/common/message/MessageType.java create mode 100644 common/src/main/java/io/xeres/common/message/chat/ChatConstants.java create mode 100644 common/src/main/java/io/xeres/common/message/chat/ChatMessage.java create mode 100644 common/src/main/java/io/xeres/common/message/chat/ChatRoomListMessage.java create mode 100644 common/src/main/java/io/xeres/common/message/chat/ChatRoomMessage.java create mode 100644 common/src/main/java/io/xeres/common/message/chat/PrivateChatMessage.java create mode 100644 common/src/main/java/io/xeres/common/message/chat/RoomInfo.java create mode 100644 common/src/main/java/io/xeres/common/message/chat/RoomType.java create mode 100644 common/src/main/java/io/xeres/common/pgp/Trust.java create mode 100644 common/src/main/java/io/xeres/common/properties/StartupProperties.java create mode 100644 common/src/main/java/io/xeres/common/protocol/NetMode.java create mode 100644 common/src/main/java/io/xeres/common/protocol/ip/IP.java create mode 100644 common/src/main/java/io/xeres/common/protocol/ip/package-info.java create mode 100644 common/src/main/java/io/xeres/common/rest/PathConfig.java create mode 100644 common/src/main/java/io/xeres/common/rest/chat/CreateChatRoomRequest.java create mode 100644 common/src/main/java/io/xeres/common/rest/config/HostnameResponse.java create mode 100644 common/src/main/java/io/xeres/common/rest/config/IpAddressRequest.java create mode 100644 common/src/main/java/io/xeres/common/rest/config/IpAddressResponse.java create mode 100644 common/src/main/java/io/xeres/common/rest/config/OwnIdentityRequest.java create mode 100644 common/src/main/java/io/xeres/common/rest/config/OwnLocationRequest.java create mode 100644 common/src/main/java/io/xeres/common/rest/config/OwnProfileRequest.java create mode 100644 common/src/main/java/io/xeres/common/rest/config/UsernameResponse.java create mode 100644 common/src/main/java/io/xeres/common/rest/location/RSIdResponse.java create mode 100644 common/src/main/java/io/xeres/common/rest/profile/CertificateRequest.java create mode 100644 common/src/main/java/io/xeres/common/util/NoSuppressedRunnable.java create mode 100644 common/src/test/java/io/xeres/common/id/IdTest.java create mode 100644 common/src/test/java/io/xeres/common/identity/TypeTest.java create mode 100644 common/src/test/java/io/xeres/common/pgp/TrustTest.java create mode 100644 common/src/test/java/io/xeres/common/protocol/ip/IPTest.java create mode 100644 common/src/test/java/io/xeres/common/protocol/ip/NetModeTest.java create mode 100644 doc/manual.adoc create mode 100644 doc/services/chat.adoc create mode 100644 doc/services/gxsid.adoc create mode 100644 doc/services/gxstrans.adoc create mode 100644 doc/services/heartbeat.adoc create mode 100644 doc/services/rtt.adoc create mode 100644 docker-compose.yml create mode 100644 gradle/wrapper/gradle-wrapper.jar create mode 100644 gradle/wrapper/gradle-wrapper.properties create mode 100644 gradlew create mode 100644 gradlew.bat create mode 100644 icon.ico create mode 100644 installer/startup.jpg create mode 100644 settings.gradle create mode 100644 ui/.gitignore create mode 100644 ui/build.gradle create mode 100644 ui/src/main/java/io/xeres/ui/JavaFxApplication.java create mode 100644 ui/src/main/java/io/xeres/ui/PrimaryStageInitializer.java create mode 100644 ui/src/main/java/io/xeres/ui/StageReadyEvent.java create mode 100644 ui/src/main/java/io/xeres/ui/UiStarter.java create mode 100644 ui/src/main/java/io/xeres/ui/client/ChatClient.java create mode 100644 ui/src/main/java/io/xeres/ui/client/ConfigClient.java create mode 100644 ui/src/main/java/io/xeres/ui/client/ConnectionClient.java create mode 100644 ui/src/main/java/io/xeres/ui/client/LocationClient.java create mode 100644 ui/src/main/java/io/xeres/ui/client/ProfileClient.java create mode 100644 ui/src/main/java/io/xeres/ui/client/message/ChatFrameHandler.java create mode 100644 ui/src/main/java/io/xeres/ui/client/message/MessageClient.java create mode 100644 ui/src/main/java/io/xeres/ui/client/message/PendingSubscription.java create mode 100644 ui/src/main/java/io/xeres/ui/client/message/SessionHandler.java create mode 100644 ui/src/main/java/io/xeres/ui/controller/Controller.java create mode 100644 ui/src/main/java/io/xeres/ui/controller/MainWindowController.java create mode 100644 ui/src/main/java/io/xeres/ui/controller/WindowController.java create mode 100644 ui/src/main/java/io/xeres/ui/controller/about/AboutWindowController.java create mode 100644 ui/src/main/java/io/xeres/ui/controller/account/AccountCreationWindowController.java create mode 100644 ui/src/main/java/io/xeres/ui/controller/chat/ChatListView.java create mode 100644 ui/src/main/java/io/xeres/ui/controller/chat/ChatRoomCreationWindowController.java create mode 100644 ui/src/main/java/io/xeres/ui/controller/chat/ChatRoomInfoController.java create mode 100644 ui/src/main/java/io/xeres/ui/controller/chat/ChatViewController.java create mode 100644 ui/src/main/java/io/xeres/ui/controller/chat/RoomHolder.java create mode 100644 ui/src/main/java/io/xeres/ui/controller/id/AddCertificateWindowController.java create mode 100644 ui/src/main/java/io/xeres/ui/controller/messaging/BroadcastWindowController.java create mode 100644 ui/src/main/java/io/xeres/ui/controller/messaging/FriendsWindowController.java create mode 100644 ui/src/main/java/io/xeres/ui/controller/messaging/MessagingWindowController.java create mode 100644 ui/src/main/java/io/xeres/ui/controller/messaging/ProfileHolder.java create mode 100644 ui/src/main/java/io/xeres/ui/controller/profile/ProfilesUiController.java create mode 100644 ui/src/main/java/io/xeres/ui/custom/ChatListCell.java create mode 100644 ui/src/main/java/io/xeres/ui/custom/NullSelectionModel.java create mode 100644 ui/src/main/java/io/xeres/ui/model/connection/Connection.java create mode 100644 ui/src/main/java/io/xeres/ui/model/connection/ConnectionMapper.java create mode 100644 ui/src/main/java/io/xeres/ui/model/location/Location.java create mode 100644 ui/src/main/java/io/xeres/ui/model/location/LocationMapper.java create mode 100644 ui/src/main/java/io/xeres/ui/model/profile/Profile.java create mode 100644 ui/src/main/java/io/xeres/ui/model/profile/ProfileMapper.java create mode 100644 ui/src/main/java/io/xeres/ui/support/tray/TrayService.java create mode 100644 ui/src/main/java/io/xeres/ui/support/util/UiUtils.java create mode 100644 ui/src/main/java/io/xeres/ui/support/window/UiWindow.java create mode 100644 ui/src/main/java/io/xeres/ui/support/window/WindowManager.java create mode 100644 ui/src/main/resources/image/icon.png create mode 100644 ui/src/main/resources/image/trayicon.png create mode 100644 ui/src/main/resources/view/about/about.fxml create mode 100644 ui/src/main/resources/view/account/account_creation.fxml create mode 100644 ui/src/main/resources/view/chat/chat_roominfo.fxml create mode 100644 ui/src/main/resources/view/chat/chatroom_create.fxml create mode 100644 ui/src/main/resources/view/chat/chatview.fxml create mode 100644 ui/src/main/resources/view/id/certificate_add.fxml create mode 100644 ui/src/main/resources/view/javafx.css create mode 100644 ui/src/main/resources/view/main.fxml create mode 100644 ui/src/main/resources/view/messaging/broadcast.fxml create mode 100644 ui/src/main/resources/view/messaging/friends.fxml create mode 100644 ui/src/main/resources/view/messaging/messaging.fxml create mode 100644 ui/src/main/resources/view/profile/profiles.fxml create mode 100644 webui/.gitignore create mode 100644 webui/build.gradle create mode 100644 webui/src/main/webapp/.browserslistrc create mode 100644 webui/src/main/webapp/.gitignore create mode 100644 webui/src/main/webapp/README.md create mode 100644 webui/src/main/webapp/angular.json create mode 100644 webui/src/main/webapp/e2e/protractor.conf.js create mode 100644 webui/src/main/webapp/e2e/src/app.e2e-spec.ts create mode 100644 webui/src/main/webapp/e2e/src/app.po.ts create mode 100644 webui/src/main/webapp/e2e/tsconfig.json create mode 100644 webui/src/main/webapp/karma.conf.js create mode 100644 webui/src/main/webapp/package-lock.json create mode 100644 webui/src/main/webapp/package.json create mode 100644 webui/src/main/webapp/src/app/account-creation/account-creation.component.css create mode 100644 webui/src/main/webapp/src/app/account-creation/account-creation.component.html create mode 100644 webui/src/main/webapp/src/app/account-creation/account-creation.component.spec.ts create mode 100644 webui/src/main/webapp/src/app/account-creation/account-creation.component.ts create mode 100644 webui/src/main/webapp/src/app/account-creation/account-creation.module.ts create mode 100644 webui/src/main/webapp/src/app/account-creation/configclient.service.ts create mode 100644 webui/src/main/webapp/src/app/app-routing.module.ts create mode 100644 webui/src/main/webapp/src/app/app.component.css create mode 100644 webui/src/main/webapp/src/app/app.component.html create mode 100644 webui/src/main/webapp/src/app/app.component.spec.ts create mode 100644 webui/src/main/webapp/src/app/app.component.ts create mode 100644 webui/src/main/webapp/src/app/app.module.ts create mode 100644 webui/src/main/webapp/src/app/shared/shared.module.ts create mode 100644 webui/src/main/webapp/src/assets/.gitkeep create mode 100644 webui/src/main/webapp/src/assets/images/logo.png create mode 100644 webui/src/main/webapp/src/environments/environment.prod.ts create mode 100644 webui/src/main/webapp/src/environments/environment.ts create mode 100644 webui/src/main/webapp/src/favicon.ico create mode 100644 webui/src/main/webapp/src/index.html create mode 100644 webui/src/main/webapp/src/main.ts create mode 100644 webui/src/main/webapp/src/polyfills.ts create mode 100644 webui/src/main/webapp/src/styles.css create mode 100644 webui/src/main/webapp/src/test.ts create mode 100644 webui/src/main/webapp/tsconfig.app.json create mode 100644 webui/src/main/webapp/tsconfig.json create mode 100644 webui/src/main/webapp/tsconfig.spec.json create mode 100644 webui/src/main/webapp/tslint.json diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 000000000..2d0603f72 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,672 @@ +root = true + +[*] +charset = utf-8 +end_of_line = lf +indent_size = 4 +indent_style = tab +insert_final_newline = false +max_line_length = 320 +tab_width = 4 +ij_continuation_indent_size = 8 +ij_formatter_off_tag = @formatter:off +ij_formatter_on_tag = @formatter:on +ij_formatter_tags_enabled = false +ij_smart_tabs = true +ij_wrap_on_typing = false + +[*.css] +indent_style = space +ij_smart_tabs = false +ij_css_align_closing_brace_with_properties = false +ij_css_blank_lines_around_nested_selector = 1 +ij_css_blank_lines_between_blocks = 1 +ij_css_enforce_quotes_on_format = false +ij_css_hex_color_long_format = false +ij_css_hex_color_lower_case = false +ij_css_hex_color_short_format = false +ij_css_hex_color_upper_case = false +ij_css_keep_blank_lines_in_code = 2 +ij_css_keep_indents_on_empty_lines = false +ij_css_keep_single_line_blocks = false +ij_css_properties_order = font,font-family,font-size,font-weight,font-style,font-variant,font-size-adjust,font-stretch,line-height,position,z-index,top,right,bottom,left,display,visibility,float,clear,overflow,overflow-x,overflow-y,clip,zoom,align-content,align-items,align-self,flex,flex-flow,flex-basis,flex-direction,flex-grow,flex-shrink,flex-wrap,justify-content,order,box-sizing,width,min-width,max-width,height,min-height,max-height,margin,margin-top,margin-right,margin-bottom,margin-left,padding,padding-top,padding-right,padding-bottom,padding-left,table-layout,empty-cells,caption-side,border-spacing,border-collapse,list-style,list-style-position,list-style-type,list-style-image,content,quotes,counter-reset,counter-increment,resize,cursor,user-select,nav-index,nav-up,nav-right,nav-down,nav-left,transition,transition-delay,transition-timing-function,transition-duration,transition-property,transform,transform-origin,animation,animation-name,animation-duration,animation-play-state,animation-timing-function,animation-delay,animation-iteration-count,animation-direction,text-align,text-align-last,vertical-align,white-space,text-decoration,text-emphasis,text-emphasis-color,text-emphasis-style,text-emphasis-position,text-indent,text-justify,letter-spacing,word-spacing,text-outline,text-transform,text-wrap,text-overflow,text-overflow-ellipsis,text-overflow-mode,word-wrap,word-break,tab-size,hyphens,pointer-events,opacity,color,border,border-width,border-style,border-color,border-top,border-top-width,border-top-style,border-top-color,border-right,border-right-width,border-right-style,border-right-color,border-bottom,border-bottom-width,border-bottom-style,border-bottom-color,border-left,border-left-width,border-left-style,border-left-color,border-radius,border-top-left-radius,border-top-right-radius,border-bottom-right-radius,border-bottom-left-radius,border-image,border-image-source,border-image-slice,border-image-width,border-image-outset,border-image-repeat,outline,outline-width,outline-style,outline-color,outline-offset,background,background-color,background-image,background-repeat,background-attachment,background-position,background-position-x,background-position-y,background-clip,background-origin,background-size,box-decoration-break,box-shadow,text-shadow +ij_css_space_after_colon = true +ij_css_space_before_opening_brace = true +ij_css_use_double_quotes = true + +[*.java] +ij_java_align_consecutive_assignments = false +ij_java_align_consecutive_variable_declarations = false +ij_java_align_group_field_declarations = false +ij_java_align_multiline_annotation_parameters = false +ij_java_align_multiline_array_initializer_expression = false +ij_java_align_multiline_assignment = false +ij_java_align_multiline_binary_operation = false +ij_java_align_multiline_chained_methods = false +ij_java_align_multiline_extends_list = false +ij_java_align_multiline_for = true +ij_java_align_multiline_method_parentheses = false +ij_java_align_multiline_parameters = true +ij_java_align_multiline_parameters_in_calls = false +ij_java_align_multiline_parenthesized_expression = false +ij_java_align_multiline_resources = true +ij_java_align_multiline_ternary_operation = false +ij_java_align_multiline_text_blocks = false +ij_java_align_multiline_throws_list = false +ij_java_align_subsequent_simple_methods = false +ij_java_align_throws_keyword = false +ij_java_annotation_parameter_wrap = off +ij_java_array_initializer_new_line_after_left_brace = false +ij_java_array_initializer_right_brace_on_new_line = false +ij_java_array_initializer_wrap = off +ij_java_assert_statement_colon_on_next_line = false +ij_java_assert_statement_wrap = off +ij_java_assignment_wrap = off +ij_java_binary_operation_sign_on_next_line = false +ij_java_binary_operation_wrap = off +ij_java_blank_lines_after_anonymous_class_header = 0 +ij_java_blank_lines_after_class_header = 0 +ij_java_blank_lines_after_imports = 1 +ij_java_blank_lines_after_package = 1 +ij_java_blank_lines_around_class = 1 +ij_java_blank_lines_around_field = 0 +ij_java_blank_lines_around_field_in_interface = 0 +ij_java_blank_lines_around_initializer = 1 +ij_java_blank_lines_around_method = 1 +ij_java_blank_lines_around_method_in_interface = 1 +ij_java_blank_lines_before_class_end = 0 +ij_java_blank_lines_before_imports = 1 +ij_java_blank_lines_before_method_body = 0 +ij_java_blank_lines_before_package = 0 +ij_java_block_brace_style = next_line +ij_java_block_comment_at_first_column = true +ij_java_call_parameters_new_line_after_left_paren = false +ij_java_call_parameters_right_paren_on_new_line = false +ij_java_call_parameters_wrap = off +ij_java_case_statement_on_separate_line = true +ij_java_catch_on_new_line = true +ij_java_class_annotation_wrap = split_into_lines +ij_java_class_brace_style = next_line +ij_java_class_count_to_use_import_on_demand = 5 +ij_java_class_names_in_javadoc = 1 +ij_java_do_not_indent_top_level_class_members = false +ij_java_do_not_wrap_after_single_annotation = false +ij_java_do_while_brace_force = never +ij_java_doc_add_blank_line_after_description = true +ij_java_doc_add_blank_line_after_param_comments = false +ij_java_doc_add_blank_line_after_return = false +ij_java_doc_add_p_tag_on_empty_lines = true +ij_java_doc_align_exception_comments = true +ij_java_doc_align_param_comments = true +ij_java_doc_do_not_wrap_if_one_line = false +ij_java_doc_enable_formatting = true +ij_java_doc_enable_leading_asterisks = true +ij_java_doc_indent_on_continuation = false +ij_java_doc_keep_empty_lines = true +ij_java_doc_keep_empty_parameter_tag = true +ij_java_doc_keep_empty_return_tag = true +ij_java_doc_keep_empty_throws_tag = true +ij_java_doc_keep_invalid_tags = true +ij_java_doc_param_description_on_new_line = false +ij_java_doc_preserve_line_breaks = false +ij_java_doc_use_throws_not_exception_tag = true +ij_java_else_on_new_line = true +ij_java_entity_dd_suffix = EJB +ij_java_entity_eb_suffix = Bean +ij_java_entity_hi_suffix = Home +ij_java_entity_lhi_prefix = Local +ij_java_entity_lhi_suffix = Home +ij_java_entity_li_prefix = Local +ij_java_entity_pk_class = java.lang.String +ij_java_entity_vo_suffix = VO +ij_java_enum_constants_wrap = off +ij_java_extends_keyword_wrap = off +ij_java_extends_list_wrap = off +ij_java_field_annotation_wrap = split_into_lines +ij_java_finally_on_new_line = true +ij_java_for_brace_force = never +ij_java_for_statement_new_line_after_left_paren = false +ij_java_for_statement_right_paren_on_new_line = false +ij_java_for_statement_wrap = off +ij_java_generate_final_locals = false +ij_java_generate_final_parameters = false +ij_java_if_brace_force = never +ij_java_imports_layout = *,|,javax.**,java.**,|,$* +ij_java_indent_case_from_switch = true +ij_java_insert_inner_class_imports = false +ij_java_insert_override_annotation = true +ij_java_keep_blank_lines_before_right_brace = 2 +ij_java_keep_blank_lines_between_package_declaration_and_header = 2 +ij_java_keep_blank_lines_in_code = 2 +ij_java_keep_blank_lines_in_declarations = 2 +ij_java_keep_control_statement_in_one_line = true +ij_java_keep_first_column_comment = true +ij_java_keep_indents_on_empty_lines = false +ij_java_keep_line_breaks = true +ij_java_keep_multiple_expressions_in_one_line = false +ij_java_keep_simple_blocks_in_one_line = false +ij_java_keep_simple_classes_in_one_line = false +ij_java_keep_simple_lambdas_in_one_line = false +ij_java_keep_simple_methods_in_one_line = false +ij_java_label_indent_absolute = false +ij_java_label_indent_size = 0 +ij_java_lambda_brace_style = end_of_line +ij_java_layout_static_imports_separately = true +ij_java_line_comment_add_space = false +ij_java_line_comment_at_first_column = true +ij_java_message_dd_suffix = EJB +ij_java_message_eb_suffix = Bean +ij_java_method_annotation_wrap = split_into_lines +ij_java_method_brace_style = next_line +ij_java_method_call_chain_wrap = off +ij_java_method_parameters_new_line_after_left_paren = false +ij_java_method_parameters_right_paren_on_new_line = false +ij_java_method_parameters_wrap = off +ij_java_modifier_list_wrap = false +ij_java_names_count_to_use_import_on_demand = 3 +ij_java_packages_to_use_import_on_demand = java.awt.*,javax.swing.* +ij_java_parameter_annotation_wrap = off +ij_java_parentheses_expression_new_line_after_left_paren = false +ij_java_parentheses_expression_right_paren_on_new_line = false +ij_java_place_assignment_sign_on_next_line = false +ij_java_prefer_longer_names = true +ij_java_prefer_parameters_wrap = false +ij_java_repeat_synchronized = true +ij_java_replace_instanceof_and_cast = false +ij_java_replace_null_check = true +ij_java_replace_sum_lambda_with_method_ref = true +ij_java_resource_list_new_line_after_left_paren = false +ij_java_resource_list_right_paren_on_new_line = false +ij_java_resource_list_wrap = off +ij_java_session_dd_suffix = EJB +ij_java_session_eb_suffix = Bean +ij_java_session_hi_suffix = Home +ij_java_session_lhi_prefix = Local +ij_java_session_lhi_suffix = Home +ij_java_session_li_prefix = Local +ij_java_session_si_suffix = Service +ij_java_space_after_closing_angle_bracket_in_type_argument = false +ij_java_space_after_colon = true +ij_java_space_after_comma = true +ij_java_space_after_comma_in_type_arguments = true +ij_java_space_after_for_semicolon = true +ij_java_space_after_quest = true +ij_java_space_after_type_cast = true +ij_java_space_before_annotation_array_initializer_left_brace = false +ij_java_space_before_annotation_parameter_list = false +ij_java_space_before_array_initializer_left_brace = false +ij_java_space_before_catch_keyword = true +ij_java_space_before_catch_left_brace = true +ij_java_space_before_catch_parentheses = true +ij_java_space_before_class_left_brace = true +ij_java_space_before_colon = true +ij_java_space_before_colon_in_foreach = true +ij_java_space_before_comma = false +ij_java_space_before_do_left_brace = true +ij_java_space_before_else_keyword = true +ij_java_space_before_else_left_brace = true +ij_java_space_before_finally_keyword = true +ij_java_space_before_finally_left_brace = true +ij_java_space_before_for_left_brace = true +ij_java_space_before_for_parentheses = true +ij_java_space_before_for_semicolon = false +ij_java_space_before_if_left_brace = true +ij_java_space_before_if_parentheses = true +ij_java_space_before_method_call_parentheses = false +ij_java_space_before_method_left_brace = true +ij_java_space_before_method_parentheses = false +ij_java_space_before_opening_angle_bracket_in_type_parameter = false +ij_java_space_before_quest = true +ij_java_space_before_switch_left_brace = true +ij_java_space_before_switch_parentheses = true +ij_java_space_before_synchronized_left_brace = true +ij_java_space_before_synchronized_parentheses = true +ij_java_space_before_try_left_brace = true +ij_java_space_before_try_parentheses = true +ij_java_space_before_type_parameter_list = false +ij_java_space_before_while_keyword = true +ij_java_space_before_while_left_brace = true +ij_java_space_before_while_parentheses = true +ij_java_space_inside_one_line_enum_braces = false +ij_java_space_within_empty_array_initializer_braces = false +ij_java_space_within_empty_method_call_parentheses = false +ij_java_space_within_empty_method_parentheses = false +ij_java_spaces_around_additive_operators = true +ij_java_spaces_around_assignment_operators = true +ij_java_spaces_around_bitwise_operators = true +ij_java_spaces_around_equality_operators = true +ij_java_spaces_around_lambda_arrow = true +ij_java_spaces_around_logical_operators = true +ij_java_spaces_around_method_ref_dbl_colon = false +ij_java_spaces_around_multiplicative_operators = true +ij_java_spaces_around_relational_operators = true +ij_java_spaces_around_shift_operators = true +ij_java_spaces_around_type_bounds_in_type_parameters = true +ij_java_spaces_around_unary_operator = false +ij_java_spaces_within_angle_brackets = false +ij_java_spaces_within_annotation_parentheses = false +ij_java_spaces_within_array_initializer_braces = false +ij_java_spaces_within_braces = false +ij_java_spaces_within_brackets = false +ij_java_spaces_within_cast_parentheses = false +ij_java_spaces_within_catch_parentheses = false +ij_java_spaces_within_for_parentheses = false +ij_java_spaces_within_if_parentheses = false +ij_java_spaces_within_method_call_parentheses = false +ij_java_spaces_within_method_parentheses = false +ij_java_spaces_within_parentheses = false +ij_java_spaces_within_switch_parentheses = false +ij_java_spaces_within_synchronized_parentheses = false +ij_java_spaces_within_try_parentheses = false +ij_java_spaces_within_while_parentheses = false +ij_java_special_else_if_treatment = true +ij_java_subclass_name_suffix = Impl +ij_java_ternary_operation_signs_on_next_line = false +ij_java_ternary_operation_wrap = off +ij_java_test_name_suffix = Test +ij_java_throws_keyword_wrap = off +ij_java_throws_list_wrap = off +ij_java_use_external_annotations = false +ij_java_use_fq_class_names = false +ij_java_use_relative_indents = false +ij_java_use_single_class_imports = true +ij_java_variable_annotation_wrap = off +ij_java_visibility = public +ij_java_while_brace_force = always +ij_java_while_on_new_line = false +ij_java_wrap_comments = false +ij_java_wrap_first_method_in_call_chain = false +ij_java_wrap_long_lines = false + +[.editorconfig] +ij_editorconfig_align_group_field_declarations = false +ij_editorconfig_space_after_colon = false +ij_editorconfig_space_after_comma = true +ij_editorconfig_space_before_colon = false +ij_editorconfig_space_before_comma = false +ij_editorconfig_spaces_around_assignment_operators = true + +[{*.bash,*.zsh,*.sh}] +indent_style = space +ij_shell_binary_ops_start_line = false +ij_shell_keep_column_alignment_padding = false +ij_shell_minify_program = false +ij_shell_redirect_followed_by_space = false +ij_shell_switch_cases_indented = false + +[{*.gant,*.groovy,*.gradle,*.gdsl,*.gy}] +indent_style = space +ij_smart_tabs = false +ij_groovy_align_group_field_declarations = false +ij_groovy_align_multiline_array_initializer_expression = false +ij_groovy_align_multiline_assignment = false +ij_groovy_align_multiline_binary_operation = false +ij_groovy_align_multiline_chained_methods = false +ij_groovy_align_multiline_extends_list = false +ij_groovy_align_multiline_for = true +ij_groovy_align_multiline_method_parentheses = false +ij_groovy_align_multiline_parameters = true +ij_groovy_align_multiline_parameters_in_calls = false +ij_groovy_align_multiline_resources = true +ij_groovy_align_multiline_ternary_operation = false +ij_groovy_align_multiline_throws_list = false +ij_groovy_align_throws_keyword = false +ij_groovy_array_initializer_new_line_after_left_brace = false +ij_groovy_array_initializer_right_brace_on_new_line = false +ij_groovy_array_initializer_wrap = off +ij_groovy_assert_statement_wrap = off +ij_groovy_assignment_wrap = off +ij_groovy_binary_operation_wrap = off +ij_groovy_blank_lines_after_class_header = 0 +ij_groovy_blank_lines_after_imports = 1 +ij_groovy_blank_lines_after_package = 1 +ij_groovy_blank_lines_around_class = 1 +ij_groovy_blank_lines_around_field = 0 +ij_groovy_blank_lines_around_field_in_interface = 0 +ij_groovy_blank_lines_around_method = 1 +ij_groovy_blank_lines_around_method_in_interface = 1 +ij_groovy_blank_lines_before_imports = 1 +ij_groovy_blank_lines_before_method_body = 0 +ij_groovy_blank_lines_before_package = 0 +ij_groovy_block_brace_style = end_of_line +ij_groovy_block_comment_at_first_column = true +ij_groovy_call_parameters_new_line_after_left_paren = false +ij_groovy_call_parameters_right_paren_on_new_line = false +ij_groovy_call_parameters_wrap = off +ij_groovy_catch_on_new_line = false +ij_groovy_class_annotation_wrap = split_into_lines +ij_groovy_class_brace_style = end_of_line +ij_groovy_do_while_brace_force = never +ij_groovy_else_on_new_line = false +ij_groovy_enum_constants_wrap = off +ij_groovy_extends_keyword_wrap = off +ij_groovy_extends_list_wrap = off +ij_groovy_field_annotation_wrap = split_into_lines +ij_groovy_finally_on_new_line = false +ij_groovy_for_brace_force = never +ij_groovy_for_statement_new_line_after_left_paren = false +ij_groovy_for_statement_right_paren_on_new_line = false +ij_groovy_for_statement_wrap = off +ij_groovy_if_brace_force = never +ij_groovy_indent_case_from_switch = true +ij_groovy_keep_blank_lines_before_right_brace = 2 +ij_groovy_keep_blank_lines_in_code = 2 +ij_groovy_keep_blank_lines_in_declarations = 2 +ij_groovy_keep_control_statement_in_one_line = true +ij_groovy_keep_first_column_comment = true +ij_groovy_keep_indents_on_empty_lines = false +ij_groovy_keep_line_breaks = true +ij_groovy_keep_multiple_expressions_in_one_line = false +ij_groovy_keep_simple_blocks_in_one_line = false +ij_groovy_keep_simple_classes_in_one_line = true +ij_groovy_keep_simple_lambdas_in_one_line = true +ij_groovy_keep_simple_methods_in_one_line = true +ij_groovy_label_indent_absolute = false +ij_groovy_label_indent_size = 0 +ij_groovy_lambda_brace_style = end_of_line +ij_groovy_line_comment_add_space = false +ij_groovy_line_comment_at_first_column = true +ij_groovy_method_annotation_wrap = split_into_lines +ij_groovy_method_brace_style = end_of_line +ij_groovy_method_call_chain_wrap = off +ij_groovy_method_parameters_new_line_after_left_paren = false +ij_groovy_method_parameters_right_paren_on_new_line = false +ij_groovy_method_parameters_wrap = off +ij_groovy_modifier_list_wrap = false +ij_groovy_parameter_annotation_wrap = off +ij_groovy_parentheses_expression_new_line_after_left_paren = false +ij_groovy_parentheses_expression_right_paren_on_new_line = false +ij_groovy_prefer_parameters_wrap = false +ij_groovy_resource_list_new_line_after_left_paren = false +ij_groovy_resource_list_right_paren_on_new_line = false +ij_groovy_resource_list_wrap = off +ij_groovy_space_after_colon = true +ij_groovy_space_after_comma = true +ij_groovy_space_after_comma_in_type_arguments = true +ij_groovy_space_after_for_semicolon = true +ij_groovy_space_after_quest = true +ij_groovy_space_after_type_cast = true +ij_groovy_space_before_annotation_parameter_list = false +ij_groovy_space_before_array_initializer_left_brace = false +ij_groovy_space_before_catch_keyword = true +ij_groovy_space_before_catch_left_brace = true +ij_groovy_space_before_catch_parentheses = true +ij_groovy_space_before_class_left_brace = true +ij_groovy_space_before_colon = true +ij_groovy_space_before_comma = false +ij_groovy_space_before_do_left_brace = true +ij_groovy_space_before_else_keyword = true +ij_groovy_space_before_else_left_brace = true +ij_groovy_space_before_finally_keyword = true +ij_groovy_space_before_finally_left_brace = true +ij_groovy_space_before_for_left_brace = true +ij_groovy_space_before_for_parentheses = true +ij_groovy_space_before_for_semicolon = false +ij_groovy_space_before_if_left_brace = true +ij_groovy_space_before_if_parentheses = true +ij_groovy_space_before_method_call_parentheses = false +ij_groovy_space_before_method_left_brace = true +ij_groovy_space_before_method_parentheses = false +ij_groovy_space_before_quest = true +ij_groovy_space_before_switch_left_brace = true +ij_groovy_space_before_switch_parentheses = true +ij_groovy_space_before_synchronized_left_brace = true +ij_groovy_space_before_synchronized_parentheses = true +ij_groovy_space_before_try_left_brace = true +ij_groovy_space_before_try_parentheses = true +ij_groovy_space_before_while_keyword = true +ij_groovy_space_before_while_left_brace = true +ij_groovy_space_before_while_parentheses = true +ij_groovy_space_within_empty_array_initializer_braces = false +ij_groovy_space_within_empty_method_call_parentheses = false +ij_groovy_spaces_around_additive_operators = true +ij_groovy_spaces_around_assignment_operators = true +ij_groovy_spaces_around_bitwise_operators = true +ij_groovy_spaces_around_equality_operators = true +ij_groovy_spaces_around_lambda_arrow = true +ij_groovy_spaces_around_logical_operators = true +ij_groovy_spaces_around_multiplicative_operators = true +ij_groovy_spaces_around_relational_operators = true +ij_groovy_spaces_around_shift_operators = true +ij_groovy_spaces_within_annotation_parentheses = false +ij_groovy_spaces_within_array_initializer_braces = false +ij_groovy_spaces_within_braces = true +ij_groovy_spaces_within_brackets = false +ij_groovy_spaces_within_cast_parentheses = false +ij_groovy_spaces_within_catch_parentheses = false +ij_groovy_spaces_within_for_parentheses = false +ij_groovy_spaces_within_if_parentheses = false +ij_groovy_spaces_within_method_call_parentheses = false +ij_groovy_spaces_within_method_parentheses = false +ij_groovy_spaces_within_parentheses = false +ij_groovy_spaces_within_switch_parentheses = false +ij_groovy_spaces_within_synchronized_parentheses = false +ij_groovy_spaces_within_try_parentheses = false +ij_groovy_spaces_within_while_parentheses = false +ij_groovy_special_else_if_treatment = true +ij_groovy_ternary_operation_wrap = off +ij_groovy_throws_keyword_wrap = off +ij_groovy_throws_list_wrap = off +ij_groovy_use_relative_indents = false +ij_groovy_variable_annotation_wrap = off +ij_groovy_while_brace_force = never +ij_groovy_while_on_new_line = false +ij_groovy_wrap_long_lines = false + +[{*.js,*.cjs}] +ij_javascript_align_imports = false +ij_javascript_align_multiline_array_initializer_expression = false +ij_javascript_align_multiline_binary_operation = false +ij_javascript_align_multiline_chained_methods = false +ij_javascript_align_multiline_extends_list = false +ij_javascript_align_multiline_for = true +ij_javascript_align_multiline_parameters = true +ij_javascript_align_multiline_parameters_in_calls = false +ij_javascript_align_multiline_ternary_operation = false +ij_javascript_align_object_properties = 0 +ij_javascript_align_union_types = false +ij_javascript_align_var_statements = 0 +ij_javascript_array_initializer_new_line_after_left_brace = false +ij_javascript_array_initializer_right_brace_on_new_line = false +ij_javascript_array_initializer_wrap = off +ij_javascript_assignment_wrap = off +ij_javascript_binary_operation_sign_on_next_line = false +ij_javascript_binary_operation_wrap = off +ij_javascript_blacklist_imports = rxjs/Rx,node_modules/**/*,@angular/material,@angular/material/typings/**,~/node_modules/**/*,@/node_modules/**/* +ij_javascript_blank_lines_after_imports = 1 +ij_javascript_blank_lines_around_class = 1 +ij_javascript_blank_lines_around_field = 0 +ij_javascript_blank_lines_around_function = 1 +ij_javascript_blank_lines_around_method = 1 +ij_javascript_block_brace_style = next_line +ij_javascript_call_parameters_new_line_after_left_paren = false +ij_javascript_call_parameters_right_paren_on_new_line = false +ij_javascript_call_parameters_wrap = off +ij_javascript_catch_on_new_line = true +ij_javascript_chained_call_dot_on_new_line = true +ij_javascript_class_brace_style = next_line +ij_javascript_comma_on_new_line = false +ij_javascript_do_while_brace_force = always +ij_javascript_else_on_new_line = true +ij_javascript_enforce_trailing_comma = keep +ij_javascript_extends_keyword_wrap = off +ij_javascript_extends_list_wrap = off +ij_javascript_field_prefix = _ +ij_javascript_file_name_style = relaxed +ij_javascript_finally_on_new_line = true +ij_javascript_for_brace_force = always +ij_javascript_for_statement_new_line_after_left_paren = false +ij_javascript_for_statement_right_paren_on_new_line = false +ij_javascript_for_statement_wrap = off +ij_javascript_force_quote_style = false +ij_javascript_force_semicolon_style = false +ij_javascript_function_expression_brace_style = next_line +ij_javascript_if_brace_force = always +ij_javascript_import_merge_members = global +ij_javascript_import_prefer_absolute_path = global +ij_javascript_import_sort_members = true +ij_javascript_import_sort_module_name = false +ij_javascript_import_use_node_resolution = true +ij_javascript_imports_wrap = on_every_item +ij_javascript_indent_case_from_switch = true +ij_javascript_indent_chained_calls = true +ij_javascript_indent_package_children = 0 +ij_javascript_jsx_attribute_value = braces +ij_javascript_keep_blank_lines_in_code = 2 +ij_javascript_keep_first_column_comment = true +ij_javascript_keep_indents_on_empty_lines = false +ij_javascript_keep_line_breaks = true +ij_javascript_keep_simple_blocks_in_one_line = false +ij_javascript_keep_simple_methods_in_one_line = false +ij_javascript_line_comment_add_space = true +ij_javascript_line_comment_at_first_column = false +ij_javascript_method_brace_style = next_line +ij_javascript_method_call_chain_wrap = off +ij_javascript_method_parameters_new_line_after_left_paren = false +ij_javascript_method_parameters_right_paren_on_new_line = false +ij_javascript_method_parameters_wrap = off +ij_javascript_object_literal_wrap = on_every_item +ij_javascript_parentheses_expression_new_line_after_left_paren = false +ij_javascript_parentheses_expression_right_paren_on_new_line = false +ij_javascript_place_assignment_sign_on_next_line = false +ij_javascript_prefer_as_type_cast = false +ij_javascript_prefer_parameters_wrap = false +ij_javascript_reformat_c_style_comments = false +ij_javascript_space_after_colon = true +ij_javascript_space_after_comma = true +ij_javascript_space_after_dots_in_rest_parameter = false +ij_javascript_space_after_generator_mult = true +ij_javascript_space_after_property_colon = true +ij_javascript_space_after_quest = true +ij_javascript_space_after_type_colon = true +ij_javascript_space_after_unary_not = false +ij_javascript_space_before_async_arrow_lparen = true +ij_javascript_space_before_catch_keyword = true +ij_javascript_space_before_catch_left_brace = true +ij_javascript_space_before_catch_parentheses = true +ij_javascript_space_before_class_lbrace = true +ij_javascript_space_before_class_left_brace = true +ij_javascript_space_before_colon = true +ij_javascript_space_before_comma = false +ij_javascript_space_before_do_left_brace = true +ij_javascript_space_before_else_keyword = true +ij_javascript_space_before_else_left_brace = true +ij_javascript_space_before_finally_keyword = true +ij_javascript_space_before_finally_left_brace = true +ij_javascript_space_before_for_left_brace = true +ij_javascript_space_before_for_parentheses = true +ij_javascript_space_before_for_semicolon = false +ij_javascript_space_before_function_left_parenth = true +ij_javascript_space_before_generator_mult = false +ij_javascript_space_before_if_left_brace = true +ij_javascript_space_before_if_parentheses = true +ij_javascript_space_before_method_call_parentheses = false +ij_javascript_space_before_method_left_brace = true +ij_javascript_space_before_method_parentheses = false +ij_javascript_space_before_property_colon = false +ij_javascript_space_before_quest = true +ij_javascript_space_before_switch_left_brace = true +ij_javascript_space_before_switch_parentheses = true +ij_javascript_space_before_try_left_brace = true +ij_javascript_space_before_type_colon = false +ij_javascript_space_before_unary_not = false +ij_javascript_space_before_while_keyword = true +ij_javascript_space_before_while_left_brace = true +ij_javascript_space_before_while_parentheses = true +ij_javascript_spaces_around_additive_operators = true +ij_javascript_spaces_around_arrow_function_operator = true +ij_javascript_spaces_around_assignment_operators = true +ij_javascript_spaces_around_bitwise_operators = true +ij_javascript_spaces_around_equality_operators = true +ij_javascript_spaces_around_logical_operators = true +ij_javascript_spaces_around_multiplicative_operators = true +ij_javascript_spaces_around_relational_operators = true +ij_javascript_spaces_around_shift_operators = true +ij_javascript_spaces_around_unary_operator = false +ij_javascript_spaces_within_array_initializer_brackets = false +ij_javascript_spaces_within_brackets = false +ij_javascript_spaces_within_catch_parentheses = false +ij_javascript_spaces_within_for_parentheses = false +ij_javascript_spaces_within_if_parentheses = false +ij_javascript_spaces_within_imports = false +ij_javascript_spaces_within_interpolation_expressions = false +ij_javascript_spaces_within_method_call_parentheses = false +ij_javascript_spaces_within_method_parentheses = false +ij_javascript_spaces_within_object_literal_braces = false +ij_javascript_spaces_within_object_type_braces = true +ij_javascript_spaces_within_parentheses = false +ij_javascript_spaces_within_switch_parentheses = false +ij_javascript_spaces_within_type_assertion = false +ij_javascript_spaces_within_union_types = true +ij_javascript_spaces_within_while_parentheses = false +ij_javascript_special_else_if_treatment = true +ij_javascript_ternary_operation_signs_on_next_line = false +ij_javascript_ternary_operation_wrap = off +ij_javascript_union_types_wrap = on_every_item +ij_javascript_use_chained_calls_group_indents = false +ij_javascript_use_double_quotes = true +ij_javascript_use_explicit_js_extension = global +ij_javascript_use_path_mapping = always +ij_javascript_use_public_modifier = false +ij_javascript_use_semicolon_after_statement = true +ij_javascript_var_declaration_wrap = normal +ij_javascript_while_brace_force = always +ij_javascript_while_on_new_line = true +ij_javascript_wrap_comments = false + +[{*.ng,*.sht,*.html,*.shtm,*.shtml,*.htm}] +indent_style = space +ij_smart_tabs = false +ij_html_add_new_line_before_tags = body,div,p,form,h1,h2,h3 +ij_html_align_attributes = true +ij_html_align_text = false +ij_html_attribute_wrap = normal +ij_html_block_comment_at_first_column = true +ij_html_do_not_align_children_of_min_lines = 0 +ij_html_do_not_break_if_inline_tags = title,h1,h2,h3,h4,h5,h6,p +ij_html_do_not_indent_children_of_tags = html,body,thead,tbody,tfoot +ij_html_enforce_quotes = false +ij_html_inline_tags = a,abbr,acronym,b,basefont,bdo,big,br,cite,cite,code,dfn,em,font,i,img,input,kbd,label,q,s,samp,select,small,span,strike,strong,sub,sup,textarea,tt,u,var +ij_html_keep_blank_lines = 2 +ij_html_keep_indents_on_empty_lines = false +ij_html_keep_line_breaks = true +ij_html_keep_line_breaks_in_text = true +ij_html_keep_whitespaces = false +ij_html_keep_whitespaces_inside = span,pre,textarea +ij_html_line_comment_at_first_column = true +ij_html_new_line_after_last_attribute = never +ij_html_new_line_before_first_attribute = never +ij_html_quote_style = double +ij_html_remove_new_line_before_tags = br +ij_html_space_after_tag_name = false +ij_html_space_around_equality_in_attribute = false +ij_html_space_inside_empty_tag = false +ij_html_text_wrap = normal + +[{*.yml,*.yaml}] +ij_yaml_keep_indents_on_empty_lines = false +ij_yaml_keep_line_breaks = true + +[{.eslintrc,.babelrc,composer.lock,.stylelintrc,jest.config,bowerrc,*.json,*.jsb3,*.jsb2}] +indent_style = space +ij_smart_tabs = false +ij_json_keep_blank_lines_in_code = 0 +ij_json_keep_indents_on_empty_lines = false +ij_json_keep_line_breaks = true +ij_json_space_after_colon = true +ij_json_space_after_comma = true +ij_json_space_before_colon = true +ij_json_space_before_comma = false +ij_json_spaces_within_braces = false +ij_json_spaces_within_brackets = false +ij_json_wrap_long_lines = false + +[{phpunit.xml.dist,*.xslt,*.xul,*.rng,*.xsl,*.xsd,*.ant,*.wadl,*.jhm,*.tld,*.fxml,*.jrxml,*.xml,*.jnlp,*.wsdl,*.wsdd,*.xjb}] +indent_style = space +ij_smart_tabs = false +ij_xml_block_comment_at_first_column = true +ij_xml_keep_indents_on_empty_lines = false +ij_xml_line_comment_at_first_column = true + +[{spring.schemas,spring.handlers,*.properties}] +ij_properties_align_group_field_declarations = false \ No newline at end of file diff --git a/.gitignore b/.gitignore new file mode 100644 index 000000000..ab971f237 --- /dev/null +++ b/.gitignore @@ -0,0 +1,9 @@ +/data/ +/data-ui/ +/data2/ +/.idea/ +/.gradle/ +./jpb/ +/out/ +/build/ +.xeres.lock \ No newline at end of file diff --git a/.jpb/persistence-units.xml b/.jpb/persistence-units.xml new file mode 100644 index 000000000..5c460eae8 --- /dev/null +++ b/.jpb/persistence-units.xml @@ -0,0 +1,12 @@ + + + + + + + + + + + + \ No newline at end of file diff --git a/LICENSE b/LICENSE new file mode 100644 index 000000000..20d40b6bc --- /dev/null +++ b/LICENSE @@ -0,0 +1,674 @@ + GNU GENERAL PUBLIC LICENSE + Version 3, 29 June 2007 + + Copyright (C) 2007 Free Software Foundation, Inc. + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + Preamble + + The GNU General Public License is a free, copyleft license for +software and other kinds of works. + + The licenses for most software and other practical works are designed +to take away your freedom to share and change the works. By contrast, +the GNU General Public License is intended to guarantee your freedom to +share and change all versions of a program--to make sure it remains free +software for all its users. We, the Free Software Foundation, use the +GNU General Public License for most of our software; it applies also to +any other work released this way by its authors. You can apply it to +your programs, too. + + When we speak of free software, we are referring to freedom, not +price. Our General Public Licenses are designed to make sure that you +have the freedom to distribute copies of free software (and charge for +them if you wish), that you receive source code or can get it if you +want it, that you can change the software or use pieces of it in new +free programs, and that you know you can do these things. + + To protect your rights, we need to prevent others from denying you +these rights or asking you to surrender the rights. Therefore, you have +certain responsibilities if you distribute copies of the software, or if +you modify it: responsibilities to respect the freedom of others. + + For example, if you distribute copies of such a program, whether +gratis or for a fee, you must pass on to the recipients the same +freedoms that you received. You must make sure that they, too, receive +or can get the source code. And you must show them these terms so they +know their rights. + + Developers that use the GNU GPL protect your rights with two steps: +(1) assert copyright on the software, and (2) offer you this License +giving you legal permission to copy, distribute and/or modify it. + + For the developers' and authors' protection, the GPL clearly explains +that there is no warranty for this free software. For both users' and +authors' sake, the GPL requires that modified versions be marked as +changed, so that their problems will not be attributed erroneously to +authors of previous versions. + + Some devices are designed to deny users access to install or run +modified versions of the software inside them, although the manufacturer +can do so. This is fundamentally incompatible with the aim of +protecting users' freedom to change the software. The systematic +pattern of such abuse occurs in the area of products for individuals to +use, which is precisely where it is most unacceptable. Therefore, we +have designed this version of the GPL to prohibit the practice for those +products. If such problems arise substantially in other domains, we +stand ready to extend this provision to those domains in future versions +of the GPL, as needed to protect the freedom of users. + + Finally, every program is threatened constantly by software patents. +States should not allow patents to restrict development and use of +software on general-purpose computers, but in those that do, we wish to +avoid the special danger that patents applied to a free program could +make it effectively proprietary. To prevent this, the GPL assures that +patents cannot be used to render the program non-free. + + The precise terms and conditions for copying, distribution and +modification follow. + + TERMS AND CONDITIONS + + 0. Definitions. + + "This License" refers to version 3 of the GNU General Public License. + + "Copyright" also means copyright-like laws that apply to other kinds of +works, such as semiconductor masks. + + "The Program" refers to any copyrightable work licensed under this +License. Each licensee is addressed as "you". "Licensees" and +"recipients" may be individuals or organizations. + + To "modify" a work means to copy from or adapt all or part of the work +in a fashion requiring copyright permission, other than the making of an +exact copy. The resulting work is called a "modified version" of the +earlier work or a work "based on" the earlier work. + + A "covered work" means either the unmodified Program or a work based +on the Program. + + To "propagate" a work means to do anything with it that, without +permission, would make you directly or secondarily liable for +infringement under applicable copyright law, except executing it on a +computer or modifying a private copy. Propagation includes copying, +distribution (with or without modification), making available to the +public, and in some countries other activities as well. + + To "convey" a work means any kind of propagation that enables other +parties to make or receive copies. Mere interaction with a user through +a computer network, with no transfer of a copy, is not conveying. + + An interactive user interface displays "Appropriate Legal Notices" +to the extent that it includes a convenient and prominently visible +feature that (1) displays an appropriate copyright notice, and (2) +tells the user that there is no warranty for the work (except to the +extent that warranties are provided), that licensees may convey the +work under this License, and how to view a copy of this License. If +the interface presents a list of user commands or options, such as a +menu, a prominent item in the list meets this criterion. + + 1. Source Code. + + The "source code" for a work means the preferred form of the work +for making modifications to it. "Object code" means any non-source +form of a work. + + A "Standard Interface" means an interface that either is an official +standard defined by a recognized standards body, or, in the case of +interfaces specified for a particular programming language, one that +is widely used among developers working in that language. + + The "System Libraries" of an executable work include anything, other +than the work as a whole, that (a) is included in the normal form of +packaging a Major Component, but which is not part of that Major +Component, and (b) serves only to enable use of the work with that +Major Component, or to implement a Standard Interface for which an +implementation is available to the public in source code form. A +"Major Component", in this context, means a major essential component +(kernel, window system, and so on) of the specific operating system +(if any) on which the executable work runs, or a compiler used to +produce the work, or an object code interpreter used to run it. + + The "Corresponding Source" for a work in object code form means all +the source code needed to generate, install, and (for an executable +work) run the object code and to modify the work, including scripts to +control those activities. However, it does not include the work's +System Libraries, or general-purpose tools or generally available free +programs which are used unmodified in performing those activities but +which are not part of the work. For example, Corresponding Source +includes interface definition files associated with source files for +the work, and the source code for shared libraries and dynamically +linked subprograms that the work is specifically designed to require, +such as by intimate data communication or control flow between those +subprograms and other parts of the work. + + The Corresponding Source need not include anything that users +can regenerate automatically from other parts of the Corresponding +Source. + + The Corresponding Source for a work in source code form is that +same work. + + 2. Basic Permissions. + + All rights granted under this License are granted for the term of +copyright on the Program, and are irrevocable provided the stated +conditions are met. This License explicitly affirms your unlimited +permission to run the unmodified Program. The output from running a +covered work is covered by this License only if the output, given its +content, constitutes a covered work. This License acknowledges your +rights of fair use or other equivalent, as provided by copyright law. + + You may make, run and propagate covered works that you do not +convey, without conditions so long as your license otherwise remains +in force. You may convey covered works to others for the sole purpose +of having them make modifications exclusively for you, or provide you +with facilities for running those works, provided that you comply with +the terms of this License in conveying all material for which you do +not control copyright. Those thus making or running the covered works +for you must do so exclusively on your behalf, under your direction +and control, on terms that prohibit them from making any copies of +your copyrighted material outside their relationship with you. + + Conveying under any other circumstances is permitted solely under +the conditions stated below. Sublicensing is not allowed; section 10 +makes it unnecessary. + + 3. Protecting Users' Legal Rights From Anti-Circumvention Law. + + No covered work shall be deemed part of an effective technological +measure under any applicable law fulfilling obligations under article +11 of the WIPO copyright treaty adopted on 20 December 1996, or +similar laws prohibiting or restricting circumvention of such +measures. + + When you convey a covered work, you waive any legal power to forbid +circumvention of technological measures to the extent such circumvention +is effected by exercising rights under this License with respect to +the covered work, and you disclaim any intention to limit operation or +modification of the work as a means of enforcing, against the work's +users, your or third parties' legal rights to forbid circumvention of +technological measures. + + 4. Conveying Verbatim Copies. + + You may convey verbatim copies of the Program's source code as you +receive it, in any medium, provided that you conspicuously and +appropriately publish on each copy an appropriate copyright notice; +keep intact all notices stating that this License and any +non-permissive terms added in accord with section 7 apply to the code; +keep intact all notices of the absence of any warranty; and give all +recipients a copy of this License along with the Program. + + You may charge any price or no price for each copy that you convey, +and you may offer support or warranty protection for a fee. + + 5. Conveying Modified Source Versions. + + You may convey a work based on the Program, or the modifications to +produce it from the Program, in the form of source code under the +terms of section 4, provided that you also meet all of these conditions: + + a) The work must carry prominent notices stating that you modified + it, and giving a relevant date. + + b) The work must carry prominent notices stating that it is + released under this License and any conditions added under section + 7. This requirement modifies the requirement in section 4 to + "keep intact all notices". + + c) You must license the entire work, as a whole, under this + License to anyone who comes into possession of a copy. This + License will therefore apply, along with any applicable section 7 + additional terms, to the whole of the work, and all its parts, + regardless of how they are packaged. This License gives no + permission to license the work in any other way, but it does not + invalidate such permission if you have separately received it. + + d) If the work has interactive user interfaces, each must display + Appropriate Legal Notices; however, if the Program has interactive + interfaces that do not display Appropriate Legal Notices, your + work need not make them do so. + + A compilation of a covered work with other separate and independent +works, which are not by their nature extensions of the covered work, +and which are not combined with it such as to form a larger program, +in or on a volume of a storage or distribution medium, is called an +"aggregate" if the compilation and its resulting copyright are not +used to limit the access or legal rights of the compilation's users +beyond what the individual works permit. Inclusion of a covered work +in an aggregate does not cause this License to apply to the other +parts of the aggregate. + + 6. Conveying Non-Source Forms. + + You may convey a covered work in object code form under the terms +of sections 4 and 5, provided that you also convey the +machine-readable Corresponding Source under the terms of this License, +in one of these ways: + + a) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by the + Corresponding Source fixed on a durable physical medium + customarily used for software interchange. + + b) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by a + written offer, valid for at least three years and valid for as + long as you offer spare parts or customer support for that product + model, to give anyone who possesses the object code either (1) a + copy of the Corresponding Source for all the software in the + product that is covered by this License, on a durable physical + medium customarily used for software interchange, for a price no + more than your reasonable cost of physically performing this + conveying of source, or (2) access to copy the + Corresponding Source from a network server at no charge. + + c) Convey individual copies of the object code with a copy of the + written offer to provide the Corresponding Source. This + alternative is allowed only occasionally and noncommercially, and + only if you received the object code with such an offer, in accord + with subsection 6b. + + d) Convey the object code by offering access from a designated + place (gratis or for a charge), and offer equivalent access to the + Corresponding Source in the same way through the same place at no + further charge. You need not require recipients to copy the + Corresponding Source along with the object code. If the place to + copy the object code is a network server, the Corresponding Source + may be on a different server (operated by you or a third party) + that supports equivalent copying facilities, provided you maintain + clear directions next to the object code saying where to find the + Corresponding Source. Regardless of what server hosts the + Corresponding Source, you remain obligated to ensure that it is + available for as long as needed to satisfy these requirements. + + e) Convey the object code using peer-to-peer transmission, provided + you inform other peers where the object code and Corresponding + Source of the work are being offered to the general public at no + charge under subsection 6d. + + A separable portion of the object code, whose source code is excluded +from the Corresponding Source as a System Library, need not be +included in conveying the object code work. + + A "User Product" is either (1) a "consumer product", which means any +tangible personal property which is normally used for personal, family, +or household purposes, or (2) anything designed or sold for incorporation +into a dwelling. In determining whether a product is a consumer product, +doubtful cases shall be resolved in favor of coverage. For a particular +product received by a particular user, "normally used" refers to a +typical or common use of that class of product, regardless of the status +of the particular user or of the way in which the particular user +actually uses, or expects or is expected to use, the product. A product +is a consumer product regardless of whether the product has substantial +commercial, industrial or non-consumer uses, unless such uses represent +the only significant mode of use of the product. + + "Installation Information" for a User Product means any methods, +procedures, authorization keys, or other information required to install +and execute modified versions of a covered work in that User Product from +a modified version of its Corresponding Source. The information must +suffice to ensure that the continued functioning of the modified object +code is in no case prevented or interfered with solely because +modification has been made. + + If you convey an object code work under this section in, or with, or +specifically for use in, a User Product, and the conveying occurs as +part of a transaction in which the right of possession and use of the +User Product is transferred to the recipient in perpetuity or for a +fixed term (regardless of how the transaction is characterized), the +Corresponding Source conveyed under this section must be accompanied +by the Installation Information. But this requirement does not apply +if neither you nor any third party retains the ability to install +modified object code on the User Product (for example, the work has +been installed in ROM). + + The requirement to provide Installation Information does not include a +requirement to continue to provide support service, warranty, or updates +for a work that has been modified or installed by the recipient, or for +the User Product in which it has been modified or installed. Access to a +network may be denied when the modification itself materially and +adversely affects the operation of the network or violates the rules and +protocols for communication across the network. + + Corresponding Source conveyed, and Installation Information provided, +in accord with this section must be in a format that is publicly +documented (and with an implementation available to the public in +source code form), and must require no special password or key for +unpacking, reading or copying. + + 7. Additional Terms. + + "Additional permissions" are terms that supplement the terms of this +License by making exceptions from one or more of its conditions. +Additional permissions that are applicable to the entire Program shall +be treated as though they were included in this License, to the extent +that they are valid under applicable law. If additional permissions +apply only to part of the Program, that part may be used separately +under those permissions, but the entire Program remains governed by +this License without regard to the additional permissions. + + When you convey a copy of a covered work, you may at your option +remove any additional permissions from that copy, or from any part of +it. (Additional permissions may be written to require their own +removal in certain cases when you modify the work.) You may place +additional permissions on material, added by you to a covered work, +for which you have or can give appropriate copyright permission. + + Notwithstanding any other provision of this License, for material you +add to a covered work, you may (if authorized by the copyright holders of +that material) supplement the terms of this License with terms: + + a) Disclaiming warranty or limiting liability differently from the + terms of sections 15 and 16 of this License; or + + b) Requiring preservation of specified reasonable legal notices or + author attributions in that material or in the Appropriate Legal + Notices displayed by works containing it; or + + c) Prohibiting misrepresentation of the origin of that material, or + requiring that modified versions of such material be marked in + reasonable ways as different from the original version; or + + d) Limiting the use for publicity purposes of names of licensors or + authors of the material; or + + e) Declining to grant rights under trademark law for use of some + trade names, trademarks, or service marks; or + + f) Requiring indemnification of licensors and authors of that + material by anyone who conveys the material (or modified versions of + it) with contractual assumptions of liability to the recipient, for + any liability that these contractual assumptions directly impose on + those licensors and authors. + + All other non-permissive additional terms are considered "further +restrictions" within the meaning of section 10. If the Program as you +received it, or any part of it, contains a notice stating that it is +governed by this License along with a term that is a further +restriction, you may remove that term. If a license document contains +a further restriction but permits relicensing or conveying under this +License, you may add to a covered work material governed by the terms +of that license document, provided that the further restriction does +not survive such relicensing or conveying. + + If you add terms to a covered work in accord with this section, you +must place, in the relevant source files, a statement of the +additional terms that apply to those files, or a notice indicating +where to find the applicable terms. + + Additional terms, permissive or non-permissive, may be stated in the +form of a separately written license, or stated as exceptions; +the above requirements apply either way. + + 8. Termination. + + You may not propagate or modify a covered work except as expressly +provided under this License. Any attempt otherwise to propagate or +modify it is void, and will automatically terminate your rights under +this License (including any patent licenses granted under the third +paragraph of section 11). + + However, if you cease all violation of this License, then your +license from a particular copyright holder is reinstated (a) +provisionally, unless and until the copyright holder explicitly and +finally terminates your license, and (b) permanently, if the copyright +holder fails to notify you of the violation by some reasonable means +prior to 60 days after the cessation. + + Moreover, your license from a particular copyright holder is +reinstated permanently if the copyright holder notifies you of the +violation by some reasonable means, this is the first time you have +received notice of violation of this License (for any work) from that +copyright holder, and you cure the violation prior to 30 days after +your receipt of the notice. + + Termination of your rights under this section does not terminate the +licenses of parties who have received copies or rights from you under +this License. If your rights have been terminated and not permanently +reinstated, you do not qualify to receive new licenses for the same +material under section 10. + + 9. Acceptance Not Required for Having Copies. + + You are not required to accept this License in order to receive or +run a copy of the Program. Ancillary propagation of a covered work +occurring solely as a consequence of using peer-to-peer transmission +to receive a copy likewise does not require acceptance. However, +nothing other than this License grants you permission to propagate or +modify any covered work. These actions infringe copyright if you do +not accept this License. Therefore, by modifying or propagating a +covered work, you indicate your acceptance of this License to do so. + + 10. Automatic Licensing of Downstream Recipients. + + Each time you convey a covered work, the recipient automatically +receives a license from the original licensors, to run, modify and +propagate that work, subject to this License. You are not responsible +for enforcing compliance by third parties with this License. + + An "entity transaction" is a transaction transferring control of an +organization, or substantially all assets of one, or subdividing an +organization, or merging organizations. If propagation of a covered +work results from an entity transaction, each party to that +transaction who receives a copy of the work also receives whatever +licenses to the work the party's predecessor in interest had or could +give under the previous paragraph, plus a right to possession of the +Corresponding Source of the work from the predecessor in interest, if +the predecessor has it or can get it with reasonable efforts. + + You may not impose any further restrictions on the exercise of the +rights granted or affirmed under this License. For example, you may +not impose a license fee, royalty, or other charge for exercise of +rights granted under this License, and you may not initiate litigation +(including a cross-claim or counterclaim in a lawsuit) alleging that +any patent claim is infringed by making, using, selling, offering for +sale, or importing the Program or any portion of it. + + 11. Patents. + + A "contributor" is a copyright holder who authorizes use under this +License of the Program or a work on which the Program is based. The +work thus licensed is called the contributor's "contributor version". + + A contributor's "essential patent claims" are all patent claims +owned or controlled by the contributor, whether already acquired or +hereafter acquired, that would be infringed by some manner, permitted +by this License, of making, using, or selling its contributor version, +but do not include claims that would be infringed only as a +consequence of further modification of the contributor version. For +purposes of this definition, "control" includes the right to grant +patent sublicenses in a manner consistent with the requirements of +this License. + + Each contributor grants you a non-exclusive, worldwide, royalty-free +patent license under the contributor's essential patent claims, to +make, use, sell, offer for sale, import and otherwise run, modify and +propagate the contents of its contributor version. + + In the following three paragraphs, a "patent license" is any express +agreement or commitment, however denominated, not to enforce a patent +(such as an express permission to practice a patent or covenant not to +sue for patent infringement). To "grant" such a patent license to a +party means to make such an agreement or commitment not to enforce a +patent against the party. + + If you convey a covered work, knowingly relying on a patent license, +and the Corresponding Source of the work is not available for anyone +to copy, free of charge and under the terms of this License, through a +publicly available network server or other readily accessible means, +then you must either (1) cause the Corresponding Source to be so +available, or (2) arrange to deprive yourself of the benefit of the +patent license for this particular work, or (3) arrange, in a manner +consistent with the requirements of this License, to extend the patent +license to downstream recipients. "Knowingly relying" means you have +actual knowledge that, but for the patent license, your conveying the +covered work in a country, or your recipient's use of the covered work +in a country, would infringe one or more identifiable patents in that +country that you have reason to believe are valid. + + If, pursuant to or in connection with a single transaction or +arrangement, you convey, or propagate by procuring conveyance of, a +covered work, and grant a patent license to some of the parties +receiving the covered work authorizing them to use, propagate, modify +or convey a specific copy of the covered work, then the patent license +you grant is automatically extended to all recipients of the covered +work and works based on it. + + A patent license is "discriminatory" if it does not include within +the scope of its coverage, prohibits the exercise of, or is +conditioned on the non-exercise of one or more of the rights that are +specifically granted under this License. You may not convey a covered +work if you are a party to an arrangement with a third party that is +in the business of distributing software, under which you make payment +to the third party based on the extent of your activity of conveying +the work, and under which the third party grants, to any of the +parties who would receive the covered work from you, a discriminatory +patent license (a) in connection with copies of the covered work +conveyed by you (or copies made from those copies), or (b) primarily +for and in connection with specific products or compilations that +contain the covered work, unless you entered into that arrangement, +or that patent license was granted, prior to 28 March 2007. + + Nothing in this License shall be construed as excluding or limiting +any implied license or other defenses to infringement that may +otherwise be available to you under applicable patent law. + + 12. No Surrender of Others' Freedom. + + If conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot convey a +covered work so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you may +not convey it at all. For example, if you agree to terms that obligate you +to collect a royalty for further conveying from those to whom you convey +the Program, the only way you could satisfy both those terms and this +License would be to refrain entirely from conveying the Program. + + 13. Use with the GNU Affero General Public License. + + Notwithstanding any other provision of this License, you have +permission to link or combine any covered work with a work licensed +under version 3 of the GNU Affero General Public License into a single +combined work, and to convey the resulting work. The terms of this +License will continue to apply to the part which is the covered work, +but the special requirements of the GNU Affero General Public License, +section 13, concerning interaction through a network will apply to the +combination as such. + + 14. Revised Versions of this License. + + The Free Software Foundation may publish revised and/or new versions of +the GNU General Public License from time to time. Such new versions will +be similar in spirit to the present version, but may differ in detail to +address new problems or concerns. + + Each version is given a distinguishing version number. If the +Program specifies that a certain numbered version of the GNU General +Public License "or any later version" applies to it, you have the +option of following the terms and conditions either of that numbered +version or of any later version published by the Free Software +Foundation. If the Program does not specify a version number of the +GNU General Public License, you may choose any version ever published +by the Free Software Foundation. + + If the Program specifies that a proxy can decide which future +versions of the GNU General Public License can be used, that proxy's +public statement of acceptance of a version permanently authorizes you +to choose that version for the Program. + + Later license versions may give you additional or different +permissions. However, no additional obligations are imposed on any +author or copyright holder as a result of your choosing to follow a +later version. + + 15. Disclaimer of Warranty. + + THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY +APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT +HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY +OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, +THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM +IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF +ALL NECESSARY SERVICING, REPAIR OR CORRECTION. + + 16. Limitation of Liability. + + IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS +THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY +GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE +USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF +DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD +PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), +EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF +SUCH DAMAGES. + + 17. Interpretation of Sections 15 and 16. + + If the disclaimer of warranty and limitation of liability provided +above cannot be given local legal effect according to their terms, +reviewing courts shall apply local law that most closely approximates +an absolute waiver of all civil liability in connection with the +Program, unless a warranty or assumption of liability accompanies a +copy of the Program in return for a fee. + + END OF TERMS AND CONDITIONS + + How to Apply These Terms to Your New Programs + + If you develop a new program, and you want it to be of the greatest +possible use to the public, the best way to achieve this is to make it +free software which everyone can redistribute and change under these terms. + + To do so, attach the following notices to the program. It is safest +to attach them to the start of each source file to most effectively +state the exclusion of warranty; and each file should have at least +the "copyright" line and a pointer to where the full notice is found. + + + Copyright (C) + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + +Also add information on how to contact you by electronic and paper mail. + + If the program does terminal interaction, make it output a short +notice like this when it starts in an interactive mode: + + Copyright (C) + This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'. + This is free software, and you are welcome to redistribute it + under certain conditions; type `show c' for details. + +The hypothetical commands `show w' and `show c' should show the appropriate +parts of the General Public License. Of course, your program's commands +might be different; for a GUI interface, you would use an "about box". + + You should also get your employer (if you work as a programmer) or school, +if any, to sign a "copyright disclaimer" for the program, if necessary. +For more information on this, and how to apply and follow the GNU GPL, see +. + + The GNU General Public License does not permit incorporating your program +into proprietary programs. If your program is a subroutine library, you +may consider it more useful to permit linking proprietary applications with +the library. If this is what you want to do, use the GNU Lesser General +Public License instead of this License. But first, please read +. \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 000000000..d9c3a91b7 --- /dev/null +++ b/README.md @@ -0,0 +1,117 @@ +# Xeres + +This is an attempted reimplementation of [Retroshare](https://retroshare.cc) in Java. + +## Supported platforms + +- Windows (x86_64) +- Linux (x86_64) +- MacOS (x86_64) _untested_ + +## Build requirements + +- Java 17 + +## Features + +- [AES-NI](https://en.wikipedia.org/wiki/AES_instruction_set) support +- [JavaFX](https://openjfx.io/) UI +- Web UI +- High concurrency + +## Download + +https://xeres.io + +## How to run + +##### IntelliJ IDEA Ultimate + +It is recommended to run the _XeresApplication_ Spring Boot configuration which is the most convenient and fastest way. +Just make sure to configure it in the following way: + +Select _Edit Configurations..._ of the _XeresApplication_ Spring Boot configuration. + +Put the following _VM options_: + + -ea -Djava.net.preferIPv4Stack=true + +And the following _Active profiles_: + + dev + +Optionally, for faster build/test turnarounds you can add in the _program arguments_: + + --fast-shutdown + +Then just run the _XeresApplication_ Spring Boot configuration. + +##### IntelliJ Community Edition + +Run the Gradle ``bootRun`` target. It's in the top right _Gradle_ panel, Tasks / application. It's already preconfigured. + +(This way also works with IntelliJ IDEA Ultimate but you'll miss some extras like colored debug output and faster launch) + +##### Command line + +###### Windows + + gradlew.bat + +###### Linux + + ./gradlew + +To pass Xeres arguments, just use the args feature, ie. + + ./gradlew bootRun --args="--no-gui --fast-shutdown" + +(Use ``--help`` to know all arguments) + +## How to setup the WebUI + +_Note: the webui is currently nonfunctional._ + +Run the gradle tasks ``installAngular`` (if you don't already have Angular installed) then ``buildAngular``. The later will create the needed files that will be served by Xeres on ``localhost:1066``. + +## Database debugging + +With IntelliJ Ultimate, create the following Database connection with the built-in Datagrip client (aka the _Database_ tool window) + +- Connection type: Embedded +- Driver: H2 +- Path: select ``./data/userdata.mv.db``. If the file is not there, run Xeres once. +- Authentication: User & Password +- User: ``sa`` +- There's no password + +## Misc + +The project was started on 2019-10-30. + +##### How to write proper git commit messages + +https://chris.beams.io/posts/git-commit/ + +##### Branching model + +The current plan is to use *master* for everything. Use a feature branch to work on a feature (ie. feature/165 if there's a ticket). Once it's ready, have someone review it then merge to master. + +Releases will use tags and release branches if further fixes are needed. + +https://reallifeprogramming.com/git-process-that-works-say-no-to-gitflow-50bf2038ccf7 + +## Useful tasks + +##### Cleaning the build directory + +run the ``clean`` task + +##### Cleaning the Angular generated directory + +run the ``cleanAngular`` task + +##### Upgrading Gradle + +- change the version in _build.gradle_ in the _wrapper_ section +- run the ``wrapper`` task diff --git a/app/.gitignore b/app/.gitignore new file mode 100644 index 000000000..e8f6f56e5 --- /dev/null +++ b/app/.gitignore @@ -0,0 +1,2 @@ +/build/ +/out/ \ No newline at end of file diff --git a/app/build.gradle b/app/build.gradle new file mode 100644 index 000000000..2c80604a6 --- /dev/null +++ b/app/build.gradle @@ -0,0 +1,130 @@ +/* + * Copyright (c) 2019-2021 by David Gerber - https://zapek.com + * + * This file is part of Xeres. + * + * Xeres is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Xeres is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Xeres. If not, see . + */ + +plugins { + id 'org.springframework.boot' + id 'org.flywaydb.flyway' + id 'jacoco' + id 'org.panteleyev.jpackageplugin' +} + +flyway { + url = "jdbc:h2:file:${project.rootDir}/data/userdata" + user = 'sa' +} + +bootJar { + manifest { + attributes 'Implementation-Version': "${project.version}" + attributes 'Implementation-Title': "${project.name}" + } +} + +bootRun { + bootRun.jvmArgs "-ea", "-Djava.net.preferIPv4Stack=true", "-Dfile.encoding=UTF-8" + bootRun.systemProperty 'spring.profiles.active', 'dev' +} + +springBoot { + buildInfo { + properties { + time = null // make the build repeatable + name = rootProject.name + } + } +} + +editorconfig { + includes = ['src/**'] + excludes = ['src/test/resources/upnp/**', 'src/main/webapp/dist/**', 'src/main/webapp/node_modules/**'] +} + +test { + useJUnitPlatform() + test.jvmArgs "-ea", "-Djava.net.preferIPv4Stack=true", "-Dfile.encoding=UTF-8" +} + +// XXX: hack to make IntelliJ actually build properly. Maybe report to JetBrains +processResources { + dependsOn ":ui:processResources" + dependsOn ":webui:processResources" +} + +task copyInstaller(type: Copy) { + from "${parent.rootDir}/installer" + include "*" + into "${project.buildDir}/${project.libsDirName}" +} + +bootBuildImage { + imageName = "zapek/${rootProject.name.toLowerCase()}:${project.version}" +} + +jpackage { + dependsOn "bootJar" + dependsOn "copyInstaller" + appName = parent.project.name + vendor = "David Gerber" + copyright = "Copyright 2019-2021 All Rights Reserved" + appDescription = "Xeres P2P Software" + input = "${project.buildDir}/${project.libsDirName}" + destination = "${project.buildDir}/dist" + mainClass = "org.springframework.boot.loader.JarLauncher" + mainJar = "app-${project.version}.jar" + icon = "${parent.rootDir}/icon.ico" + licenseFile = "${parent.rootDir}/LICENSE" + javaOptions = ['-Djava.net.preferIPv4Stack=true', '-Dfile.encoding=UTF-8', '-splash:$APPDIR/startup.jpg'] + windows { + winMenu = true + winPerUserInstall = true + winDirChooser = true + winMenuGroup = "Xeres" + } + linux { + linuxShortcut = true + } +} + +dependencies { + annotationProcessor 'org.springframework.boot:spring-boot-configuration-processor' + implementation project(':common') + implementation project(':ui') + implementation project(':webui') + implementation 'org.springframework.boot:spring-boot-starter-json' + implementation 'org.springframework.boot:spring-boot-starter-data-jpa' + implementation 'org.springframework.boot:spring-boot-starter-validation' + runtimeOnly 'com.h2database:h2:1.4.200' + implementation 'org.springframework.boot:spring-boot-starter-web' + implementation 'org.springframework.boot:spring-boot-starter-websocket' + implementation 'org.springframework.boot:spring-boot-starter-actuator' + implementation 'org.springframework.boot:spring-boot-starter-webflux' // to bring in netty + implementation "org.flywaydb:flyway-core:$flywayVersion" + implementation "org.bouncycastle:bcpg-jdk15on:$bouncycastleVersion" + implementation "org.bouncycastle:bcpkix-jdk15on:$bouncycastleVersion" + implementation 'org.jsoup:jsoup:1.14.2' + implementation 'com.github.peter-gergely-horvath:windpapi4j:1.0' + implementation 'net.harawata:appdirs:1.2.1' + implementation 'com.github.atomashpolskiy:bt-dht:1.9' + implementation "org.apache.commons:commons-lang3:$apacheCommonsLangVersion" + implementation "org.apache.commons:commons-collections4:$apacheCommonsCollectionsVersion" + implementation "org.springdoc:springdoc-openapi-ui:$springOpenApi" + implementation 'io.netty:netty-tcnative-boringssl-static:2.0.43.Final' + developmentOnly 'org.springframework.boot:spring-boot-devtools' + testImplementation 'org.springframework.boot:spring-boot-starter-test' +} diff --git a/app/src/main/java/io/xeres/app/XeresApplication.java b/app/src/main/java/io/xeres/app/XeresApplication.java new file mode 100644 index 000000000..2bdc0a3a2 --- /dev/null +++ b/app/src/main/java/io/xeres/app/XeresApplication.java @@ -0,0 +1,59 @@ +/* + * Copyright (c) 2019-2021 by David Gerber - https://zapek.com + * + * This file is part of Xeres. + * + * Xeres is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Xeres is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Xeres. If not, see . + */ + +package io.xeres.app; + +import io.xeres.app.application.environment.CommandArgument; +import io.xeres.app.application.environment.HostVariable; +import io.xeres.common.properties.StartupProperties; +import io.xeres.ui.UiStarter; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; + +import static io.xeres.app.application.environment.Cloud.isRunningOnCloud; + +@SpringBootApplication(scanBasePackageClasses = {io.xeres.app.XeresApplication.class, io.xeres.ui.UiStarter.class}) +public class XeresApplication +{ + private static final Logger log = LoggerFactory.getLogger(XeresApplication.class); + + public static void main(String[] args) + { + HostVariable.parse(); + CommandArgument.parse(args); + + if (isRunningOnCloud() || !StartupProperties.getBoolean(StartupProperties.Property.UI, true)) + { + log.info("no gui mode"); + SpringApplication.run(XeresApplication.class, args); + } + else + { + log.info("gui mode"); + UiStarter.start(XeresApplication.class, args); // this starts spring as well + } + } + + public static boolean isRemoteUiClient() + { + return "none".equals(System.getProperty("spring.main.web-application-type")); + } +} diff --git a/app/src/main/java/io/xeres/app/application/SingleInstanceRun.java b/app/src/main/java/io/xeres/app/application/SingleInstanceRun.java new file mode 100644 index 000000000..e2d626e45 --- /dev/null +++ b/app/src/main/java/io/xeres/app/application/SingleInstanceRun.java @@ -0,0 +1,108 @@ +/* + * Copyright (c) 2019-2021 by David Gerber - https://zapek.com + * + * This file is part of Xeres. + * + * Xeres is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Xeres is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Xeres. If not, see . + */ + +package io.xeres.app.application; + + +import io.xeres.common.AppName; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.File; +import java.io.IOException; +import java.io.RandomAccessFile; +import java.nio.channels.FileLock; +import java.nio.file.Files; +import java.nio.file.NoSuchFileException; +import java.util.Locale; +import java.util.Optional; + +/** + * Utility class to detect if an application is already running. + */ +public final class SingleInstanceRun +{ + private static final Logger log = LoggerFactory.getLogger(SingleInstanceRun.class); + + private static final String LOCK_FILE = "." + AppName.NAME.toLowerCase(Locale.ROOT) + ".lock"; + + private static File lockFile; + private static RandomAccessFile randomAccessFile; + private static FileLock fileLock; + + private SingleInstanceRun() + { + throw new UnsupportedOperationException("Utility class"); + } + + /** + * Enforce an application to have a single instance of itself given a certain directory. + * + * @param dataDir the directory to be used by the application + * @return true if the application can run without conflicts, false if it's already running + */ + public static boolean enforceSingleInstance(String dataDir) + { + lockFile = new File(dataDir, LOCK_FILE); + + var result = false; + try + { + randomAccessFile = new RandomAccessFile(lockFile, "rw"); + + fileLock = Optional.ofNullable(randomAccessFile.getChannel().tryLock()).orElseThrow(IllegalStateException::new); + if (fileLock != null) + { + result = true; + Runtime.getRuntime().addShutdownHook(new Thread(new ShutdownHook())); + } + } + catch (IOException | IllegalStateException | IllegalArgumentException e) + { + log.debug("Couldn't enforce single instance: {}.", e.getMessage()); + } + catch (SecurityException e) + { + log.warn("Shutdown hook denied by SecurityManager; There will be a dangling lock file at {}", LOCK_FILE); + } + return result; + } + + private static class ShutdownHook implements Runnable + { + @Override + public void run() + { + try + { + fileLock.release(); + randomAccessFile.close(); + Files.delete(lockFile.toPath()); + } + catch (NoSuchFileException e) + { + log.warn("Lockfile missing. Was it deleted manually?"); + } + catch (IOException | SecurityException e) + { + log.warn("Failed to delete lockfile: {}. Is it locked by some external process?", e.getClass()); + } + } + } +} diff --git a/app/src/main/java/io/xeres/app/application/Startup.java b/app/src/main/java/io/xeres/app/application/Startup.java new file mode 100644 index 000000000..b7512b7c7 --- /dev/null +++ b/app/src/main/java/io/xeres/app/application/Startup.java @@ -0,0 +1,295 @@ +/* + * Copyright (c) 2019-2021 by David Gerber - https://zapek.com + * + * This file is part of Xeres. + * + * Xeres is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Xeres is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Xeres. If not, see . + */ + +package io.xeres.app.application; + +import io.netty.util.ResourceLeakDetector; +import io.xeres.app.XeresApplication; +import io.xeres.app.application.events.LocationReadyEvent; +import io.xeres.app.configuration.DataDirConfiguration; +import io.xeres.app.database.DatabaseSession; +import io.xeres.app.database.DatabaseSessionManager; +import io.xeres.app.database.model.connection.Connection; +import io.xeres.app.net.bdisc.BroadcastDiscoveryService; +import io.xeres.app.net.dht.DHTService; +import io.xeres.app.net.peer.PeerConnectionManager; +import io.xeres.app.net.protocol.PeerAddress; +import io.xeres.app.net.upnp.UPNPService; +import io.xeres.app.properties.NetworkProperties; +import io.xeres.app.service.ChatRoomService; +import io.xeres.app.service.LocationService; +import io.xeres.app.service.PeerService; +import io.xeres.app.service.PrefsService; +import io.xeres.app.xrs.service.RsServiceRegistry; +import io.xeres.common.AppName; +import io.xeres.common.protocol.ip.IP; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.boot.ApplicationArguments; +import org.springframework.boot.ApplicationRunner; +import org.springframework.boot.info.BuildProperties; +import org.springframework.context.ApplicationEventPublisher; +import org.springframework.context.event.ContextClosedEvent; +import org.springframework.context.event.EventListener; +import org.springframework.core.env.Environment; +import org.springframework.stereotype.Component; + +import java.awt.*; +import java.nio.charset.Charset; +import java.nio.charset.StandardCharsets; +import java.nio.file.Path; +import java.util.Optional; +import java.util.concurrent.CompletableFuture; +import java.util.stream.Collectors; + +import static io.xeres.app.application.environment.Cloud.isRunningOnCloud; +import static java.util.function.Predicate.not; + +@Component +public class Startup implements ApplicationRunner +{ + private static final Logger log = LoggerFactory.getLogger(Startup.class); + + private final PeerService peerService; + private final UPNPService upnpService; + private final BroadcastDiscoveryService broadcastDiscoveryService; + private final DHTService dhtService; + private final LocationService locationService; + private final PrefsService prefsService; + private final BuildProperties buildProperties; + private final Environment environment; + private final ApplicationEventPublisher publisher; + private final NetworkProperties networkProperties; + private final DatabaseSessionManager databaseSessionManager; + private final DataDirConfiguration dataDirConfiguration; + private final ChatRoomService chatRoomService; + private final PeerConnectionManager peerConnectionManager; + + public Startup(PeerService peerService, UPNPService upnpService, BroadcastDiscoveryService broadcastDiscoveryService, DHTService dhtService, LocationService locationService, PrefsService prefsService, BuildProperties buildProperties, Environment environment, ApplicationEventPublisher publisher, NetworkProperties networkProperties, DatabaseSessionManager databaseSessionManager, DataDirConfiguration dataDirConfiguration, ChatRoomService chatRoomService, PeerConnectionManager peerConnectionManager) + { + this.peerService = peerService; + this.upnpService = upnpService; + this.broadcastDiscoveryService = broadcastDiscoveryService; + this.dhtService = dhtService; + this.locationService = locationService; + this.prefsService = prefsService; + this.buildProperties = buildProperties; + this.environment = environment; + this.publisher = publisher; + this.networkProperties = networkProperties; + this.databaseSessionManager = databaseSessionManager; + this.dataDirConfiguration = dataDirConfiguration; + this.chatRoomService = chatRoomService; + this.peerConnectionManager = peerConnectionManager; + } + + @Override + public void run(ApplicationArguments args) + { + // This is a convenient place to start code as it works in both UI and non-UI mode + checkSingleInstance(); + showStartupInfo(); + checkRequirements(); + showCapabilities(); + showFeatures(); + if (log.isDebugEnabled()) + { + showDebug(); + } + + hideSplashScreen(); + + if (XeresApplication.isRemoteUiClient()) + { + log.info("Remote UI mode"); + return; + } + + if (prefsService.isOwnProfilePresent()) + { + try (var session = new DatabaseSession(databaseSessionManager)) + { + var location = locationService.findOwnLocation().orElseThrow(); + String localIpAddress = Optional.ofNullable(IP.getLocalIpAddress()).orElseThrow(() -> new IllegalStateException("Current host has no IP address. Please configure your network")); + + // Get the previously saved port. If there isn't any because there was an + // error on initialization, simply try to get a new one. + // XXX: should --server-port change this? + int localPort = location.getConnections().stream() + .filter(not(Connection::isExternal)) + .findFirst() + .orElseGet(() -> Connection.from(PeerAddress.from(localIpAddress, IP.getFreeLocalPort()))) + .getPort(); + + // Send the event asynchronously so that our transaction can complete first + CompletableFuture.runAsync(() -> publisher.publishEvent(new LocationReadyEvent(localIpAddress, localPort))); + } + } + else + { + log.info("Waiting... Use the user interface to send commands to create a profile"); + } + } + + /** + * Called when the network is ready (aka we have a location). + * + * @param event the LocationReadyEvent + */ + @EventListener + public void onApplicationEvent(LocationReadyEvent event) + { + try (var session = new DatabaseSession(databaseSessionManager)) + { + locationService.markAllConnectionsAsDisconnected(); + chatRoomService.markAllChatRoomsAsLeft(); + + log.info("Starting network services..."); + var ownAddress = PeerAddress.from(event.localIpAddress(), event.localPort()); + if (ownAddress.isValid()) + { + locationService.updateConnection(locationService.findOwnLocation().orElseThrow(), ownAddress); + if (ownAddress.isLAN()) + { + log.info("We are on a LAN. Launching UPNP and Broadcast discovery..."); + upnpService.start(event.localIpAddress(), event.localPort()); + broadcastDiscoveryService.start(event.localIpAddress(), event.localPort()); + } + if (networkProperties.isDht()) + { + dhtService.start(event.localPort()); + } + peerService.start(); + } + else + { + log.error("Local address is invalid: {}, can't start network services", event.localIpAddress()); + } + } + } + + @EventListener // We don't use @PreDestroy because netty uses other beans on shutdown and we don't want them in shutdown state already + public void onApplicationEvent(ContextClosedEvent event) + { + backupUserData(); + + log.info("Shutting down..."); + peerConnectionManager.shutdown(); + dhtService.stop(); + upnpService.stop(); + broadcastDiscoveryService.stop(); + peerService.stop(); + + upnpService.waitForTermination(); + } + + private void backupUserData() + { + // XXX: find a smarter way to do backups. either provide a way to do them manually, by external script or every X time or while running + if (dataDirConfiguration.getDataDir() != null) + { + var backupFile = Path.of(dataDirConfiguration.getDataDir(), "backup.zip").toString(); + log.info("Doing backup of database to {}", backupFile); + prefsService.backup(backupFile); + } + } + + private void showStartupInfo() + { + log.info("Startup sequence ({}, {}, {})", + buildProperties.getName(), + buildProperties.getVersion(), + environment.getActiveProfiles().length > 0 ? environment.getActiveProfiles()[0] : "prod"); + } + + private void showCapabilities() + { + long totalMemory = Runtime.getRuntime().totalMemory(); + log.info("OS: {} ({})", System.getProperty("os.name"), System.getProperty("os.arch")); + log.info("JRE: {} {} ({})", System.getProperty("java.vendor"), System.getProperty("java.version"), System.getProperty("java.home")); + log.info("Charset: {}", Charset.defaultCharset()); + log.debug("Working directory: {}", System.getProperty("user.dir")); + log.info("Number of processor threads: {}", Runtime.getRuntime().availableProcessors()); + log.info("Memory allocated for the JVM: {} MB", totalMemory / 1024 / 1024); + log.info("Maximum allocatable memory: {} MB", Runtime.getRuntime().maxMemory() / 1024 / 1024); + } + + private void showFeatures() + { + if (log.isDebugEnabled()) + { + log.debug("Network features: {}", networkProperties.getFeatures()); + log.debug("Services: {}", RsServiceRegistry.getServices().stream().map(rsService -> rsService.getServiceType().getName()).collect(Collectors.joining(", "))); + } + } + + private void showDebug() + { + if (ResourceLeakDetector.isEnabled()) + { + log.debug("Netty leak detector level: {}", ResourceLeakDetector.getLevel()); + } + else + { + log.debug("Netty leak detector disabled"); + } + } + + private void hideSplashScreen() + { + if (isRunningOnCloud()) + { + return; + } + + // XXX: how to avoid the splash screen with --no-gui? + System.setProperty("java.awt.headless", "false"); // XXX: same problem as in 'ui'... should the splash screen be handled by the 'ui' module? + try + { + var splashScreen = SplashScreen.getSplashScreen(); + + // XXX: we could print stuff over the splashscreen while starting, see: https://docs.oracle.com/javase/tutorial/uiswing/misc/splashscreen.html + if (splashScreen != null) + { + splashScreen.close(); + } + } + catch (Exception | UnsatisfiedLinkError e) + { + // Apparently that splashscreen stuff is brittle + log.error("Error while trying to close splash screen: {}", e.getMessage()); + } + } + + private void checkRequirements() + { + if (Charset.defaultCharset() != StandardCharsets.UTF_8) + { + throw new IllegalArgumentException("Platform charset must be UTF-8, found: " + Charset.defaultCharset()); + } + } + + private void checkSingleInstance() + { + if (!SingleInstanceRun.enforceSingleInstance(dataDirConfiguration.getDataDir())) + { + throw new IllegalStateException("An instance of " + AppName.NAME + " is already running."); + } + } +} diff --git a/app/src/main/java/io/xeres/app/application/environment/Cloud.java b/app/src/main/java/io/xeres/app/application/environment/Cloud.java new file mode 100644 index 000000000..e7dde65c8 --- /dev/null +++ b/app/src/main/java/io/xeres/app/application/environment/Cloud.java @@ -0,0 +1,49 @@ +/* + * Copyright (c) 2019-2020 by David Gerber - https://zapek.com + * + * This file is part of Xeres. + * + * Xeres is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Xeres is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Xeres. If not, see . + */ + +package io.xeres.app.application.environment; + +import java.util.Arrays; + +/** + * Utility class containing cloud related functions. + */ +public final class Cloud +{ + private Cloud() + { + throw new UnsupportedOperationException("Utility class"); + } + + /** + * Checks if we are running on the cloud. This is done by checking if the profile cloud is in the SPRING_PROFILES_ACTIVE env variable. + * + * @return true if running on the cloud + */ + public static boolean isRunningOnCloud() + { + String profiles = System.getenv("SPRING_PROFILES_ACTIVE"); + if (profiles != null) + { + String[] tokens = profiles.split(","); + return Arrays.asList(tokens).contains("cloud"); + } + return false; + } +} diff --git a/app/src/main/java/io/xeres/app/application/environment/CommandArgument.java b/app/src/main/java/io/xeres/app/application/environment/CommandArgument.java new file mode 100644 index 000000000..057188572 --- /dev/null +++ b/app/src/main/java/io/xeres/app/application/environment/CommandArgument.java @@ -0,0 +1,181 @@ +/* + * Copyright (c) 2019-2021 by David Gerber - https://zapek.com + * + * This file is part of Xeres. + * + * Xeres is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Xeres is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Xeres. If not, see . + */ + +package io.xeres.app.application.environment; + +import io.xeres.common.AppName; +import io.xeres.common.properties.StartupProperties; +import org.springframework.boot.ApplicationArguments; +import org.springframework.boot.DefaultApplicationArguments; + +import java.io.BufferedReader; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.util.List; + +/** + * Utility class to handle user supplied command line arguments. + */ +public final class CommandArgument +{ + private CommandArgument() + { + throw new UnsupportedOperationException("Utility class"); + } + + private static final String HELP = "help"; + private static final String VERSION = "version"; + private static final String NO_GUI = "no-gui"; + private static final String DATA_DIR = "data-dir"; + private static final String CONTROL_PORT = "control-port"; + private static final String SERVER_PORT = "server-port"; + private static final String FAST_SHUTDOWN = "fast-shutdown"; + public static final String SERVER_ONLY = "server-only"; + public static final String REMOTE_CONNECT = "remote-connect"; + + /** + * Parses command line arguments. Should be called before Spring Boot is initialized. + * + * @param args the command line arguments + */ + public static void parse(String[] args) + { + ApplicationArguments appArgs = new DefaultApplicationArguments(args); + + for (String arg : appArgs.getNonOptionArgs()) + { + switch (arg) + { + case "-h", "-help", "help" -> showHelp(); + default -> throw new IllegalArgumentException("Unknown argument [" + arg + "]. Run with the --help argument."); + } + } + + for (String arg : appArgs.getOptionNames()) + { + switch (arg) + { + case HELP -> showHelp(); + case VERSION -> showVersion(); + case DATA_DIR -> setString(StartupProperties.Property.DATA_DIR, appArgs, arg); + case CONTROL_PORT -> { + setPort(StartupProperties.Property.CONTROL_PORT, appArgs, arg); + setPort(StartupProperties.Property.UI_PORT, appArgs, arg); + } + case SERVER_PORT -> setPort(StartupProperties.Property.SERVER_PORT, appArgs, arg); + case REMOTE_CONNECT -> { + String ipAndPort = appArgs.getOptionValues(arg).stream() + .findFirst() + .orElseThrow(() -> new IllegalArgumentException(REMOTE_CONNECT + " must specify a host or host:port like 'localhost' or 'localhost:1066'")); + StartupProperties.setUiRemoteConnect(ipAndPort); + } + case NO_GUI -> setBooleanInverted(StartupProperties.Property.UI, appArgs, arg); + case FAST_SHUTDOWN -> setBoolean(StartupProperties.Property.FAST_SHUTDOWN, appArgs, arg); + case SERVER_ONLY -> setBoolean(StartupProperties.Property.SERVER_ONLY, appArgs, arg); + default -> throw new IllegalArgumentException("Unknown argument " + arg); + } + } + } + + private static void setBoolean(StartupProperties.Property property, ApplicationArguments appArgs, String arg) + { + if (!appArgs.getOptionValues(arg).isEmpty()) + { + throw new IllegalArgumentException("--" + arg + " doesn't expect a value"); + } + StartupProperties.setBoolean(property, "true"); + } + + private static void setBooleanInverted(StartupProperties.Property property, ApplicationArguments appArgs, String arg) + { + if (!appArgs.getOptionValues(arg).isEmpty()) + { + throw new IllegalArgumentException("--" + arg + " doesn't expect a value"); + } + StartupProperties.setBoolean(property, "false"); + } + + private static void setString(StartupProperties.Property property, ApplicationArguments appArgs, String arg) + { + try + { + StartupProperties.setString(property, getValue(appArgs, arg)); + } + catch (IllegalArgumentException e) + { + throw new IllegalArgumentException("--" + arg + " does not contain a value"); + } + } + + private static void setPort(StartupProperties.Property property, ApplicationArguments appArgs, String arg) + { + try + { + StartupProperties.setPort(property, getValue(appArgs, arg)); + } + catch (IllegalArgumentException e) + { + throw new IllegalArgumentException("--" + arg + " must specify a port bigger than 0 and smaller than 65536"); + } + } + + private static String getValue(ApplicationArguments appArgs, String arg) + { + List optionValues = appArgs.getOptionValues(arg); + if (optionValues.isEmpty()) + { + throw new IllegalArgumentException("--" + arg + " expects a value"); + } + else if (optionValues.size() > 1) + { + throw new IllegalArgumentException("--" + arg + " cannot be specified more than once"); + } + return optionValues.get(0); + } + + private static void showHelp() + { + System.out.print("Usage: " + AppName.NAME + " [--options]\n" + + "where options include:\n" + + " --no-gui start without an UI\n" + + " --data-dir= specify the data directory\n" + + " --control-port= specify the control port for remote access\n" + + " --server-port= specify the local port to bind to for incoming peer connections\n" + + " --fast-shutdown ignore proper shutdown procedure (not recommended)\n" + + " --server-only only accept incoming connections, do not make outgoing ones\n" + + " --remote-connect=[:] act as an UI client only and connect to a remote server\n" + + " --version print the version of the software\n" + + " --help print this help message\n" + + "See https://xeres.io/docs/ for more details.\n" + ); + System.exit(0); + } + + private static void showVersion() + { + InputStream buildInfo = CommandArgument.class.getClassLoader().getResourceAsStream("META-INF/build-info.properties"); + if (buildInfo != null) + { + var reader = new BufferedReader(new InputStreamReader(buildInfo)); + reader.lines().filter(s -> s.startsWith("build.version=")) + .forEach(s -> System.out.println(AppName.NAME + " " + s.substring(s.indexOf('=') + 1))); + } + System.exit(0); + } +} diff --git a/app/src/main/java/io/xeres/app/application/environment/HostVariable.java b/app/src/main/java/io/xeres/app/application/environment/HostVariable.java new file mode 100644 index 000000000..22f20aabf --- /dev/null +++ b/app/src/main/java/io/xeres/app/application/environment/HostVariable.java @@ -0,0 +1,117 @@ +/* + * Copyright (c) 2019-2021 by David Gerber - https://zapek.com + * + * This file is part of Xeres. + * + * Xeres is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Xeres is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Xeres. If not, see . + */ + +package io.xeres.app.application.environment; + +import io.xeres.common.properties.StartupProperties; +import io.xeres.common.properties.StartupProperties.Property; + +import java.util.Optional; + +import static io.xeres.common.properties.StartupProperties.Property.*; + +/** + * This utility class allows to set properties using the content of env variables. + * This is especially useful when run from containers. + */ +public final class HostVariable +{ + /** + * The location of the data directory. Either an absolute or a relative path. + */ + public static final String XERES_DATA_DIR = "XERES_DATA_DIR"; + + /** + * The control port of the server (ie. where the UI client is sending commands to). + */ + public static final String XERES_CONTROL_PORT = "XERES_CONTROL_PORT"; + + /** + * The incoming port for peer connections. + */ + public static final String XERES_SERVER_PORT = "XERES_SERVER_PORT"; + + /** + * If we are running in server mode only (ie. we're only accepting incoming connections). + * Ideal for a chat server. + */ + public static final String XERES_SERVER_ONLY = "XERES_SERVER_ONLY"; + + private static final String ENVIRONMENT_VARIABLE_STRING = "Environment variable"; + + private HostVariable() + { + throw new UnsupportedOperationException("Utility class"); + } + + /** + * Sets properties using env variables. + */ + public static void parse() + { + get(XERES_DATA_DIR).ifPresent(s -> setString(XERES_DATA_DIR, DATA_DIR, s)); + get(XERES_SERVER_ONLY).ifPresent(s -> setBoolean(XERES_SERVER_ONLY, SERVER_ONLY, s)); + get(XERES_CONTROL_PORT).ifPresent(s -> { + setPort(XERES_CONTROL_PORT, CONTROL_PORT, s); + setPort(XERES_CONTROL_PORT, UI_PORT, s); + }); + get(XERES_SERVER_PORT).ifPresent(s -> setPort(XERES_SERVER_PORT, SERVER_PORT, s)); + } + + private static Optional get(String key) + { + return Optional.ofNullable(System.getenv(key)); + } + + private static void setString(String name, Property property, String value) + { + try + { + StartupProperties.setString(property, value); + } + catch (IllegalArgumentException e) + { + throw new IllegalArgumentException(ENVIRONMENT_VARIABLE_STRING + " " + name + " does not contain a value"); + } + } + + private static void setBoolean(String name, Property property, String value) + { + try + { + StartupProperties.setBoolean(property, value); + } + catch (IllegalArgumentException e) + { + throw new IllegalArgumentException(ENVIRONMENT_VARIABLE_STRING + " " + name + " does not contain a boolean value (" + value + ")"); + } + } + + private static void setPort(String name, Property property, String value) + { + try + { + StartupProperties.setPort(property, value); + } + catch (IllegalArgumentException e) + { + throw new IllegalArgumentException(ENVIRONMENT_VARIABLE_STRING + " " + name + " does not contain a valid port bigger than 0 and smaller than 65536 (" + value + ")"); + } + } +} diff --git a/app/src/main/java/io/xeres/app/application/events/LocationReadyEvent.java b/app/src/main/java/io/xeres/app/application/events/LocationReadyEvent.java new file mode 100644 index 000000000..86a8dfe3a --- /dev/null +++ b/app/src/main/java/io/xeres/app/application/events/LocationReadyEvent.java @@ -0,0 +1,28 @@ +/* + * Copyright (c) 2019-2021 by David Gerber - https://zapek.com + * + * This file is part of Xeres. + * + * Xeres is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Xeres is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Xeres. If not, see . + */ + +package io.xeres.app.application.events; + +/** + * Event that is sent once the application has a location (eg. a profile + location has been created) + * and is thus ready to connect to peers. + */ +public record LocationReadyEvent(String localIpAddress, int localPort) +{ +} diff --git a/app/src/main/java/io/xeres/app/application/events/package-info.java b/app/src/main/java/io/xeres/app/application/events/package-info.java new file mode 100644 index 000000000..e425e3f0c --- /dev/null +++ b/app/src/main/java/io/xeres/app/application/events/package-info.java @@ -0,0 +1,26 @@ +/* + * Copyright (c) 2019-2020 by David Gerber - https://zapek.com + * + * This file is part of Xeres. + * + * Xeres is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Xeres is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Xeres. If not, see . + */ + +/** + * This package contains Spring application events. + * + * Beware: those events are synchronous which means they'll run in the thread sending them. If you need asynchronous events, + * send them with CompletableFuture.async(). But prefer the REST API if possible. + */ +package io.xeres.app.application.events; diff --git a/app/src/main/java/io/xeres/app/configuration/DataDirConfiguration.java b/app/src/main/java/io/xeres/app/configuration/DataDirConfiguration.java new file mode 100644 index 000000000..d39b77663 --- /dev/null +++ b/app/src/main/java/io/xeres/app/configuration/DataDirConfiguration.java @@ -0,0 +1,142 @@ +/* + * Copyright (c) 2019-2021 by David Gerber - https://zapek.com + * + * This file is part of Xeres. + * + * Xeres is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Xeres is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Xeres. If not, see . + */ + +package io.xeres.app.configuration; + +import io.xeres.common.AppName; +import io.xeres.common.properties.StartupProperties; +import net.harawata.appdirs.AppDirsFactory; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.core.env.Environment; +import org.springframework.core.env.Profiles; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; + +/** + * Configuration for everything related to the user data directory (database, keys, user data, ...). + */ +@Configuration +public class DataDirConfiguration +{ + private static final String LOCAL_DATA = "data"; + + private final Environment environment; + + private String dataDir; + + public DataDirConfiguration(Environment environment) + { + this.environment = environment; + } + + /** + * Gets the data directory where all user data is stored. + * + * @return the path to the data directory + */ + @Bean + public String getDataDir() + { + if (dataDir != null) + { + return dataDir; + } + + // If a datasource is already set (ie. tests), then we don't return anything + if (environment.getProperty("spring.datasource.url") != null) + { + return null; + } + + dataDir = getDataDirFromArgs(); + if (dataDir == null && environment.acceptsProfiles(Profiles.of("dev"))) + { + dataDir = getDataDirFromDevelopmentSetup(); + } + + if (dataDir == null) + { + dataDir = getDataDirFromPortableFileLocation(); + } + if (dataDir == null) + { + dataDir = getDataDirFromNativePlatform(); + } + + assert dataDir != null; + + var path = Path.of(dataDir); + if (Files.notExists(path)) + { + try + { + Files.createDirectory(path); + } + catch (IOException e) + { + throw new IllegalStateException("Couldn't create data directory: " + dataDir + ", :" + e.getMessage()); + } + } + return dataDir; + } + + private String getDataDirFromArgs() + { + return StartupProperties.getString(StartupProperties.Property.DATA_DIR); + } + + private String getDataDirFromPortableFileLocation() + { + var portable = Path.of("portable"); + if (Files.exists(portable)) + { + return portable.resolveSibling(LOCAL_DATA).toAbsolutePath().toString(); + } + return null; + } + + private String getDataDirFromNativePlatform() + { + var appDirs = AppDirsFactory.getInstance(); + return appDirs.getUserDataDir(AppName.NAME, null, null, true); + } + + private String getDataDirFromDevelopmentSetup() + { + // Find out if we're running from rootProject, which means + // we have an 'app' folder in there. + // We use a relative directory because currentDir is not supposed + // to change and it looks clearer. + var appDir = Path.of("app"); + if (Files.exists(appDir)) + { + return Path.of(".", LOCAL_DATA).toString(); + } + appDir = Path.of("..", "app"); + if (Files.exists(appDir)) + { + return Path.of("..", LOCAL_DATA).toString(); + } + throw new IllegalStateException("Unable to find/create data directory. Current directory must be the project's root directory or 'app'. It is " + Paths.get("").toAbsolutePath()); + } +} diff --git a/app/src/main/java/io/xeres/app/configuration/DataSourceConfiguration.java b/app/src/main/java/io/xeres/app/configuration/DataSourceConfiguration.java new file mode 100644 index 000000000..1da8cf075 --- /dev/null +++ b/app/src/main/java/io/xeres/app/configuration/DataSourceConfiguration.java @@ -0,0 +1,85 @@ +/* + * Copyright (c) 2019-2021 by David Gerber - https://zapek.com + * + * This file is part of Xeres. + * + * Xeres is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Xeres is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Xeres. If not, see . + */ + +package io.xeres.app.configuration; + +import io.xeres.app.properties.DatabaseProperties; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.boot.jdbc.DataSourceBuilder; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.DependsOn; +import org.springframework.core.env.Environment; +import org.springframework.core.env.Profiles; + +import javax.sql.DataSource; +import java.nio.file.Path; + +/** + * Configuration for the location and options of the database. + */ +@Configuration +@DependsOn("getDataDir") +public class DataSourceConfiguration +{ + private static final Logger log = LoggerFactory.getLogger(DataSourceConfiguration.class); + + private final Environment environment; + private final DatabaseProperties databaseProperties; + private final DataDirConfiguration dataDirConfiguration; + + public DataSourceConfiguration(Environment environment, DatabaseProperties databaseProperties, DataDirConfiguration dataDirConfiguration) + { + this.environment = environment; + this.databaseProperties = databaseProperties; + this.dataDirConfiguration = dataDirConfiguration; + } + + @Bean + @ConditionalOnProperty(prefix = "spring.datasource", name = "url", havingValue = "false", matchIfMissing = true) + public DataSource getDataSource() + { + var useJMX = ""; + + if (environment.acceptsProfiles(Profiles.of("dev"))) + { + useJMX = ";JMX=TRUE"; + } + + var dataDir = Path.of(dataDirConfiguration.getDataDir(), "userdata").toString(); + + log.debug("Using database file: {}", dataDir); + + var dbOpts = ""; + + if (databaseProperties.getCacheSize() != null) + { + dbOpts += ";CACHE_SIZE=" + databaseProperties.getCacheSize(); + } + + return DataSourceBuilder + .create() + .url("jdbc:h2:file:" + dataDir + dbOpts + useJMX) + .username("sa") + .driverClassName("org.h2.Driver") + .build(); + } +} diff --git a/app/src/main/java/io/xeres/app/configuration/SchedulerConfiguration.java b/app/src/main/java/io/xeres/app/configuration/SchedulerConfiguration.java new file mode 100644 index 000000000..4f9cf0515 --- /dev/null +++ b/app/src/main/java/io/xeres/app/configuration/SchedulerConfiguration.java @@ -0,0 +1,32 @@ +/* + * Copyright (c) 2019-2020 by David Gerber - https://zapek.com + * + * This file is part of Xeres. + * + * Xeres is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Xeres is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Xeres. If not, see . + */ + +package io.xeres.app.configuration; + +import org.springframework.context.annotation.Configuration; +import org.springframework.scheduling.annotation.EnableScheduling; + +/** + * Configuration of the scheduler. Just enables it. + */ +@Configuration +@EnableScheduling +public class SchedulerConfiguration +{ +} diff --git a/app/src/main/java/io/xeres/app/configuration/WebSocketConfiguration.java b/app/src/main/java/io/xeres/app/configuration/WebSocketConfiguration.java new file mode 100644 index 000000000..1d0214426 --- /dev/null +++ b/app/src/main/java/io/xeres/app/configuration/WebSocketConfiguration.java @@ -0,0 +1,77 @@ +/* + * Copyright (c) 2019-2021 by David Gerber - https://zapek.com + * + * This file is part of Xeres. + * + * Xeres is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Xeres is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Xeres. If not, see . + */ + +package io.xeres.app.configuration; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.event.EventListener; +import org.springframework.messaging.simp.config.MessageBrokerRegistry; +import org.springframework.web.socket.config.annotation.EnableWebSocketMessageBroker; +import org.springframework.web.socket.config.annotation.StompEndpointRegistry; +import org.springframework.web.socket.config.annotation.WebSocketMessageBrokerConfigurer; +import org.springframework.web.socket.messaging.SessionDisconnectEvent; +import org.springframework.web.socket.messaging.SessionSubscribeEvent; +import org.springframework.web.socket.messaging.SessionUnsubscribeEvent; + +import static io.xeres.common.rest.PathConfig.CHAT_PATH; + +/** + * Configuration of the WebSocket. This is used for anything that requires a persistent connection from + * the UI client to the server because of a bidirectional data stream (ie. chat windows). + */ +@Configuration +@EnableWebSocketMessageBroker +public class WebSocketConfiguration implements WebSocketMessageBrokerConfigurer +{ + private static final Logger log = LoggerFactory.getLogger(WebSocketConfiguration.class); + + @Override + public void registerStompEndpoints(StompEndpointRegistry registry) + { + registry.addEndpoint("/ws"); + } + + @Override + public void configureMessageBroker(MessageBrokerRegistry registry) + { + registry.setApplicationDestinationPrefixes("/app"); // this is for @Controller annotated endpoints + registry.enableSimpleBroker(CHAT_PATH); // this is for the broker (subscriptions, ...) + } + + // XXX: the following is useful for debugging... remove them once I'm done and if I don't need it (they could still be useful to DETECT if a client fails to subscribe to websockets) + @EventListener + public void handleSessionSubscribeEvent(SessionSubscribeEvent event) + { + log.debug("Subscription from {}", event); + } + + @EventListener + public void handleSessionUnsubscribeEvent(SessionUnsubscribeEvent event) + { + log.debug("Unsubscription from {}", event); // XXX: seems to not be called?! + } + + @EventListener + public void handleSessionDisconnectEvent(SessionDisconnectEvent event) + { + log.debug("Disconnection from {}", event); + } +} diff --git a/app/src/main/java/io/xeres/app/crypto/chatcipher/ChatChallenge.java b/app/src/main/java/io/xeres/app/crypto/chatcipher/ChatChallenge.java new file mode 100644 index 000000000..5ad3fd39d --- /dev/null +++ b/app/src/main/java/io/xeres/app/crypto/chatcipher/ChatChallenge.java @@ -0,0 +1,52 @@ +/* + * Copyright (c) 2019-2021 by David Gerber - https://zapek.com + * + * This file is part of Xeres. + * + * Xeres is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Xeres is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Xeres. If not, see . + */ + +package io.xeres.app.crypto.chatcipher; + +import io.xeres.common.id.Identifier; + +/** + * Utility class to handle challenge codes, which allows peers to know if they + * have a common private chat room without disclosing it first. + */ +public final class ChatChallenge +{ + private ChatChallenge() + { + throw new UnsupportedOperationException("Utility class"); + } + + public static long code(Identifier identifier, long chatRoomId, long messageId) + { + long code = 0; + + byte[] id = identifier.getBytes(); + + for (var i = 0; i < identifier.getBytes().length; i++) + { + code += messageId; + code ^= code >>> 35; + code += code << 6; + code ^= id[i] * chatRoomId; + code += code << 26; + code ^= code >>> 13; + } + return code; + } +} diff --git a/app/src/main/java/io/xeres/app/crypto/pgp/PGP.java b/app/src/main/java/io/xeres/app/crypto/pgp/PGP.java new file mode 100644 index 000000000..97f1478db --- /dev/null +++ b/app/src/main/java/io/xeres/app/crypto/pgp/PGP.java @@ -0,0 +1,303 @@ +/* + * Copyright (c) 2019-2021 by David Gerber - https://zapek.com + * + * This file is part of Xeres. + * + * Xeres is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Xeres is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Xeres. If not, see . + */ + +package io.xeres.app.crypto.pgp; + +import io.xeres.app.crypto.rsa.RSA; +import org.bouncycastle.bcpg.ArmoredOutputStream; +import org.bouncycastle.bcpg.BCPGOutputStream; +import org.bouncycastle.openpgp.*; +import org.bouncycastle.openpgp.operator.PGPDigestCalculator; +import org.bouncycastle.openpgp.operator.bc.BcKeyFingerprintCalculator; +import org.bouncycastle.openpgp.operator.bc.BcPGPContentSignerBuilder; +import org.bouncycastle.openpgp.operator.bc.BcPGPContentVerifierBuilderProvider; +import org.bouncycastle.openpgp.operator.jcajce.*; + +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.nio.ByteBuffer; +import java.security.InvalidKeyException; +import java.security.SignatureException; +import java.util.Date; +import java.util.List; + +import static org.bouncycastle.bcpg.HashAlgorithmTags.SHA1; +import static org.bouncycastle.bcpg.HashAlgorithmTags.SHA512; +import static org.bouncycastle.bcpg.PublicKeyAlgorithmTags.DSA; +import static org.bouncycastle.bcpg.PublicKeyAlgorithmTags.RSA_SIGN; +import static org.bouncycastle.openpgp.PGPEncryptedData.CAST5; +import static org.bouncycastle.openpgp.PGPPublicKey.RSA_GENERAL; +import static org.bouncycastle.openpgp.PGPSignature.BINARY_DOCUMENT; +import static org.bouncycastle.openpgp.PGPSignature.DEFAULT_CERTIFICATION; +import static org.bouncycastle.openpgp.PGPUtil.SHA256; + +/** + * Utility class containing all PGP related methods. + */ +public final class PGP +{ + private PGP() + { + throw new UnsupportedOperationException("Utility class"); + } + + public enum Armor + { + NONE, + BASE64 + } + + /** + * Gets the PGP public key as an armored (ASCII) key. + * @param pgpPublicKey the public key + * @param out the output stream + * @throws IOException I/O error + */ + public static void getPublicKeyArmored(PGPPublicKey pgpPublicKey, OutputStream out) throws IOException + { + getPublicKeyArmored(pgpPublicKey.getEncoded(true), out); + } + + /** + * Gets the PGP public key as an armored (ASCII) key. + * @param data the public key as a byte array + * @param out the output stream + * @throws IOException I/O error + */ + public static void getPublicKeyArmored(byte[] data, OutputStream out) throws IOException + { + var aOut = new ArmoredOutputStream(out); + + var pgpObjectFactory = new PGPObjectFactory(data, new BcKeyFingerprintCalculator()); + + var object = pgpObjectFactory.nextObject(); + + if (object instanceof PGPPublicKeyRing pgpPublicKeyRing) + { + for (PGPPublicKey publicKey : pgpPublicKeyRing) + { + publicKey.encode(aOut); + aOut.close(); + } + } + else + { + throw new IllegalArgumentException("Wrong encoded key structure: " + object.getClass().getCanonicalName()); + } + } + + /** + * Gets the PGP secret key. While a secret key needs a password to be converted to a private + * key, this implementation uses an empty password. + * + * @param data a byte array containing the raw PGP key + * @return the {@link PGPSecretKey} + * @throws IllegalArgumentException if the key is wrong + */ + public static PGPSecretKey getPGPSecretKey(byte[] data) + { + var pgpObjectFactory = new PGPObjectFactory(data, new BcKeyFingerprintCalculator()); + + try + { + var object = pgpObjectFactory.nextObject(); + + if (object instanceof PGPSecretKeyRing pgpSecretKeyRing) + { + if (!pgpSecretKeyRing.iterator().hasNext()) + { + throw new IllegalArgumentException("PGPSecretKeyRing is empty"); + } + return pgpSecretKeyRing.iterator().next(); + } + else + { + throw new IllegalArgumentException("PGPSecretKeyRing expected, got: " + object.getClass().getCanonicalName() + " instead"); + } + } + catch (IOException e) + { + throw new IllegalArgumentException("PGPSecretKeyRing is corrupted", e); + } + } + + /** + * Gets the PGP public key. + * + * @param data a byte array containing the raw PGP key + * @return the {@link PGPPublicKey} + * @throws InvalidKeyException if the key is wrong + */ + public static PGPPublicKey getPGPPublicKey(byte[] data) throws InvalidKeyException + { + var pgpObjectFactory = new PGPObjectFactory(data, new BcKeyFingerprintCalculator()); + + try + { + var object = pgpObjectFactory.nextObject(); + + if (object instanceof PGPPublicKeyRing pgpPublicKeyRing) + { + if (!pgpPublicKeyRing.iterator().hasNext()) + { + throw new InvalidKeyException("PGPPublicKeyRing is empty"); + } + return pgpPublicKeyRing.iterator().next(); + } + else + { + throw new InvalidKeyException("PGPPublicKeyRing expected, got: " + object.getClass().getCanonicalName() + " instead"); + } + } + catch (IOException e) + { + throw new InvalidKeyException("PGPPublicKeyRing is corrupted", e); + } + } + + /** + * Generates a PGP secret key. The key is a PGP V4 format, RSA key with a default certification, + * SHA-1 integrity checksum and encrypted with CAST5. The packet sizes are encoded using the original format.

+ * This is the most compatible PGP key, yet considered secure as of 2020. Do not attempt to change it. + * + * @param id the id of the key + * @param suffix the suffix appended to the id + * @param size the size of the key + * @return the {@link PGPSecretKey} + * @throws PGPException if somehow the PGP key generation failed (ie. wrong key size) + */ + public static PGPSecretKey generateSecretKey(String id, String suffix, int size) throws PGPException + { + var keyPair = RSA.generateKeys(size); + + PGPDigestCalculator sha1Calc = new JcaPGPDigestCalculatorProviderBuilder().build().get(SHA1); + + PGPKeyPair pgpKeyPair = new JcaPGPKeyPair(RSA_GENERAL, keyPair, new Date()); + + return new PGPSecretKey(DEFAULT_CERTIFICATION, pgpKeyPair, suffix != null ? (id + " " + suffix) : id, sha1Calc, null, null, + new JcaPGPContentSignerBuilder(pgpKeyPair.getPublicKey().getAlgorithm(), SHA1), + new JcePBESecretKeyEncryptorBuilder(CAST5, sha1Calc) + .setProvider("BC").build("".toCharArray())); + } + + /** + * Signs a message as a binary document using SHA-256. + * + * @param pgpSecretKey the secret key to sign the message with + * @param in the message + * @param out the resulting PGP signature + * @param armor optional ASCII armoring (base 64 encoding) + * @throws PGPException PGP error + * @throws IOException I/O error + */ + public static void sign(PGPSecretKey pgpSecretKey, InputStream in, OutputStream out, Armor armor) throws PGPException, IOException + { + if (armor == Armor.BASE64) + { + out = new ArmoredOutputStream(out); + } + + var pgpPrivateKey = pgpSecretKey.extractPrivateKey(new JcePBESecretKeyDecryptorBuilder().setProvider("BC").build("".toCharArray())); + + var signatureGenerator = new PGPSignatureGenerator(new BcPGPContentSignerBuilder(pgpSecretKey.getPublicKey().getAlgorithm(), SHA256)); + + signatureGenerator.init(BINARY_DOCUMENT, pgpPrivateKey); + + var bOut = new BCPGOutputStream(out); + + signatureGenerator.update(in.readAllBytes()); + in.close(); + + signatureGenerator.generate().encode(bOut); + + if (armor == Armor.BASE64) + { + out.close(); + } + } + + /** + * Verifies a PGP signature. Note that only a handful of algorithms are supported. + * + * @param pgpPublicKey the public key corresponding to the private key used to generate the signature + * @param signature the signature + * @param in the message + * @throws SignatureException if the message verification failed + * @throws IOException I/O error + * @throws PGPException PGP error + */ + public static void verify(PGPPublicKey pgpPublicKey, byte[] signature, InputStream in) throws IOException, SignatureException, PGPException + { + var pgpObjectFactory = new PGPObjectFactory(signature, new BcKeyFingerprintCalculator()); + + var object = pgpObjectFactory.nextObject(); + if (!(object instanceof PGPSignatureList pgpSignatures)) + { + throw new SignatureException("Signature doesn't contain a PGP signature list"); + } + if (pgpSignatures.isEmpty()) + { + throw new SignatureException("Signature list empty"); + } + + var pgpSignature = pgpSignatures.get(0); + + if (pgpSignature.getSignatureType() != BINARY_DOCUMENT) + { + throw new SignatureException("Signature is not of BINARY_DOCUMENT (" + pgpSignature.getSignatureType() + ")"); + } + + if (pgpSignature.getVersion() != 4) + { + throw new SignatureException("Signature is not PGP version 4 (" + pgpSignature.getVersion() + ")"); + } + + if (!List.of(RSA_GENERAL, RSA_SIGN, DSA).contains(pgpSignature.getKeyAlgorithm())) + { + throw new SignatureException("Signature key algorithm is not of RSA or DSA (" + pgpSignature.getSignatureType() + ")"); + } + + if (!List.of(SHA1, SHA256, SHA512).contains(pgpSignature.getHashAlgorithm())) + { + throw new SignatureException("Signature hash algorithm is not of SHA family (" + pgpSignature.getHashAlgorithm() + ")"); + } + + pgpSignature.init(new BcPGPContentVerifierBuilderProvider(), pgpPublicKey); + pgpSignature.update(in.readAllBytes()); + in.close(); + if (!pgpSignature.verify()) + { + throw new SignatureException("Wrong signature"); + } + } + + /** + * Gets the PGP identifier, which is the last long of the PGP fingerprint + * + * @return the PGP identifier + */ + public static long getPGPIdentifierFromFingerprint(byte[] fingerprint) + { + var buf = ByteBuffer.allocate(Long.BYTES); + buf.put(fingerprint, 12, 8); + buf.flip(); + return buf.getLong(); + } +} diff --git a/app/src/main/java/io/xeres/app/crypto/pgp/PGPSigner.java b/app/src/main/java/io/xeres/app/crypto/pgp/PGPSigner.java new file mode 100644 index 000000000..6092266e7 --- /dev/null +++ b/app/src/main/java/io/xeres/app/crypto/pgp/PGPSigner.java @@ -0,0 +1,74 @@ +/* + * Copyright (c) 2019-2020 by David Gerber - https://zapek.com + * + * This file is part of Xeres. + * + * Xeres is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Xeres is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Xeres. If not, see . + */ + +package io.xeres.app.crypto.pgp; + +import org.bouncycastle.asn1.pkcs.PKCSObjectIdentifiers; +import org.bouncycastle.asn1.x509.AlgorithmIdentifier; +import org.bouncycastle.openpgp.PGPException; +import org.bouncycastle.openpgp.PGPSecretKey; +import org.bouncycastle.operator.ContentSigner; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.OutputStream; + +import static io.xeres.app.crypto.pgp.PGP.Armor; +import static io.xeres.app.crypto.pgp.PGP.sign; + +public class PGPSigner implements ContentSigner +{ + private final ByteArrayOutputStream outputStream; + private final PGPSecretKey pgpSecretKey; + + public PGPSigner(PGPSecretKey pgpSecretKey) + { + this.pgpSecretKey = pgpSecretKey; + outputStream = new ByteArrayOutputStream(); + } + + @Override + public AlgorithmIdentifier getAlgorithmIdentifier() + { + return new AlgorithmIdentifier(PKCSObjectIdentifiers.sha256WithRSAEncryption); + } + + @Override + public OutputStream getOutputStream() + { + return outputStream; + } + + @Override + public byte[] getSignature() + { + try (var out = new ByteArrayOutputStream()) + { + sign(pgpSecretKey, new ByteArrayInputStream(outputStream.toByteArray()), out, Armor.NONE); + outputStream.close(); + + return out.toByteArray(); + } + catch (PGPException | IOException e) + { + throw new IllegalStateException("Failed to sign certificate: " + e.getMessage(), e.getCause()); + } + } +} diff --git a/app/src/main/java/io/xeres/app/crypto/pgp/package-info.java b/app/src/main/java/io/xeres/app/crypto/pgp/package-info.java new file mode 100644 index 000000000..dec9166df --- /dev/null +++ b/app/src/main/java/io/xeres/app/crypto/pgp/package-info.java @@ -0,0 +1,29 @@ +/* + * Copyright (c) 2019-2020 by David Gerber - https://zapek.com + * + * This file is part of Xeres. + * + * Xeres is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Xeres is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Xeres. If not, see . + */ + +/** + * Implements all PGP related functions. Used for creating the private and public PGP keys + * which identify one profile, also known as a user. Locations' certificates are then signed using + * the private key.

+ * The public key is distributed to other profiles so that they can verify the location's certificate + * signature. + * + * @see RFC 4880 + */ +package io.xeres.app.crypto.pgp; diff --git a/app/src/main/java/io/xeres/app/crypto/rsa/RSA.java b/app/src/main/java/io/xeres/app/crypto/rsa/RSA.java new file mode 100644 index 000000000..03e2e656f --- /dev/null +++ b/app/src/main/java/io/xeres/app/crypto/rsa/RSA.java @@ -0,0 +1,131 @@ +/* + * Copyright (c) 2019-2021 by David Gerber - https://zapek.com + * + * This file is part of Xeres. + * + * Xeres is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Xeres is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Xeres. If not, see . + */ + +package io.xeres.app.crypto.rsa; + +import java.security.*; +import java.security.spec.InvalidKeySpecException; +import java.security.spec.PKCS8EncodedKeySpec; +import java.security.spec.X509EncodedKeySpec; + +/** + * Implements all RSA related functions. Used for creating the private and public SSL keys + * which identify one location, also known as a machine or node. + */ +public final class RSA +{ + private static final String KEY_ALGORITHM = "RSA"; + private static final String SIGNATURE_ALGORITHM = "SHA1withRSA"; + + private RSA() + { + throw new UnsupportedOperationException("Utility class"); + } + + /** + * Generates a RSA private/public key pair. + * + * @param size the key size (512, 1024, 2048, 3072, 4096, etc...) + * @return the key pair + */ + public static KeyPair generateKeys(int size) + { + try + { + var keyPairGenerator = KeyPairGenerator.getInstance(KEY_ALGORITHM); + + keyPairGenerator.initialize(size); + + return keyPairGenerator.generateKeyPair(); + } + catch (NoSuchAlgorithmException e) + { + throw new IllegalArgumentException("Algorithm not supported"); + } + } + + /** + * Gets the RSA public key from the encoded form. + * + * @param data the public key in encoded bytes + * @return the public key + * @throws NoSuchAlgorithmException RSA algorithm unavailable + * @throws InvalidKeySpecException Not an RSA key + */ + public static PublicKey getPublicKey(byte[] data) throws NoSuchAlgorithmException, InvalidKeySpecException + { + return KeyFactory.getInstance(KEY_ALGORITHM).generatePublic(new X509EncodedKeySpec(data)); + } + + /** + * Gets the RSA private key from the encoded form. + * + * @param data the private key in encoded bytes + * @return the private key + * @throws NoSuchAlgorithmException RSA algorithm unavailable + * @throws InvalidKeySpecException Not an RSA key + */ + public static PrivateKey getPrivateKey(byte[] data) throws NoSuchAlgorithmException, InvalidKeySpecException + { + return KeyFactory.getInstance(KEY_ALGORITHM).generatePrivate(new PKCS8EncodedKeySpec(data)); + } + + /** + * Signs some data. + * @param data the data to sign + * @param privateKey the RSA private key + * @return the signature + */ + public static byte[] sign(byte[] data, PrivateKey privateKey) + { + try + { + var signer = Signature.getInstance(SIGNATURE_ALGORITHM); + signer.initSign(privateKey); + signer.update(data); + return signer.sign(); + } + catch (NoSuchAlgorithmException | InvalidKeyException | SignatureException e) + { + throw new IllegalArgumentException(e); + } + } + + /** + * Verifies signed data. + * @param publicKey the RSA public key + * @param signature the signature + * @param data the data to verify + * @return true if verification is successful + */ + public static boolean verify(PublicKey publicKey, byte[] signature, byte[] data) + { + try + { + var signer = Signature.getInstance(SIGNATURE_ALGORITHM); + signer.initVerify(publicKey); + signer.update(data); + return signer.verify(signature); + } + catch (NoSuchAlgorithmException | SignatureException | InvalidKeyException e) + { + throw new IllegalArgumentException(e); + } + } +} diff --git a/app/src/main/java/io/xeres/app/crypto/rsid/RSId.java b/app/src/main/java/io/xeres/app/crypto/rsid/RSId.java new file mode 100644 index 000000000..ec38e1e52 --- /dev/null +++ b/app/src/main/java/io/xeres/app/crypto/rsid/RSId.java @@ -0,0 +1,153 @@ +/* + * Copyright (c) 2019-2021 by David Gerber - https://zapek.com + * + * This file is part of Xeres. + * + * Xeres is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Xeres is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Xeres. If not, see . + */ + +package io.xeres.app.crypto.rsid; + +import io.xeres.app.crypto.pgp.PGP; +import io.xeres.app.crypto.rsid.certificate.RSCertificate; +import io.xeres.app.crypto.rsid.shortinvite.ShortInvite; +import io.xeres.app.net.protocol.PeerAddress; +import io.xeres.common.id.LocationId; +import org.apache.commons.lang3.StringUtils; +import org.bouncycastle.openpgp.PGPPublicKey; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.security.cert.CertificateParsingException; +import java.util.Set; + +public abstract class RSId +{ + public enum Type + { + SHORT_INVITE, + BOTH + } + + public static RSId parse(String data) throws CertificateParsingException + { + if (StringUtils.isBlank(data)) + { + throw new CertificateParsingException("Empty input"); + } + + try + { + return ShortInvite.parseShortInvite(data); + } + catch (CertificateParsingException e) + { + // XXX: this is not very nice... how do I know which parsing failed? + return RSCertificate.parseRSCertificate(data); + } + } + + // XXX: the names need to be adjusted... + + public abstract boolean hasInternalIp(); + + public abstract PeerAddress getInternalIp(); + + public abstract boolean hasExternalIp(); + + public abstract PeerAddress getExternalIp(); + + public abstract boolean hasPgpPublicKey(); + + public abstract PGPPublicKey getPgpPublicKey(); + + public abstract byte[] getPgpFingerprint(); + + public abstract boolean hasName(); + + public abstract String getName(); + + public abstract boolean hasLocationInfo(); + + public abstract LocationId getLocationId(); + + public abstract boolean hasDnsName(); + + public abstract PeerAddress getDnsName(); + + public abstract boolean isHiddenNode(); + + public abstract PeerAddress getHiddenNodeAddress(); + + public abstract boolean hasLocators(); + + public abstract Set getLocators(); + + /** + * Gets the PGP identifier, which is the last long of the PGP fingerprint + * + * @return the PGP identifier + */ + public Long getPgpIdentifier() + { + byte[] bytes = getPgpFingerprint(); + + if (bytes == null) + { + return null; + } + return PGP.getPGPIdentifierFromFingerprint(bytes); + } + + protected static byte[] cleanupInput(byte[] data) + { + try (var out = new ByteArrayOutputStream()) + { + for (byte b : data) + { + if (b == ' ' || b == '\n' || b == '\t' || b == '\r') + { + continue; + } + out.write(b); + } + return out.toByteArray(); + } + catch (IOException e) + { + throw new IllegalStateException(e); + } + } + + protected static int getPacketSize(InputStream in) throws IOException + { + int octet1 = in.read(); + + if (octet1 < 192) // size is coded in one byte + { + return octet1; + } + else if (octet1 < 224) // size is coded in 2 bytes + { + int octet2 = in.read(); + + return ((octet1 - 192) << 8) + octet2 + 192; + } + else + { + throw new IllegalArgumentException("Unsupported packet data size"); + } + } +} diff --git a/app/src/main/java/io/xeres/app/crypto/rsid/RSIdArmor.java b/app/src/main/java/io/xeres/app/crypto/rsid/RSIdArmor.java new file mode 100644 index 000000000..3a6d583c7 --- /dev/null +++ b/app/src/main/java/io/xeres/app/crypto/rsid/RSIdArmor.java @@ -0,0 +1,226 @@ +/* + * Copyright (c) 2019-2021 by David Gerber - https://zapek.com + * + * This file is part of Xeres. + * + * Xeres is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Xeres is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Xeres. If not, see . + */ + +package io.xeres.app.crypto.rsid; + +import io.xeres.app.crypto.rsid.certificate.RSCertificate; +import io.xeres.app.crypto.rsid.certificate.RSCertificateTags; +import io.xeres.app.crypto.rsid.shortinvite.ShortInvite; +import io.xeres.app.crypto.rsid.shortinvite.ShortInviteQuirks; +import io.xeres.app.crypto.rsid.shortinvite.ShortInviteTags; +import io.xeres.common.id.Identifier; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.OutputStream; +import java.nio.charset.StandardCharsets; +import java.util.Base64; + +public final class RSIdArmor +{ + private static final Logger log = LoggerFactory.getLogger(RSIdArmor.class); + + private enum WrapMode + { + CONTINUOUS, + SLICED + } + + private RSIdArmor() + { + throw new UnsupportedOperationException("Utility class"); + } + + /** + * Gets an armored version of the certificate or short invite. It's encoded using base64 and can be + * used in emails, forums, etc... + * + * @param rsId the RSId + * @return an ascii armored version of it + * @throws IOException I/O error + */ + public static String getArmored(RSId rsId) throws IOException + { + try (var out = new ByteArrayOutputStream()) + { + if (rsId instanceof RSCertificate) + { + return getArmoredCertificate(rsId, out); + } + else if (rsId instanceof ShortInvite) + { + return getArmoredShortInvite(rsId, out); + } + else + { + throw new UnsupportedOperationException("Armor mode not implemented"); + } + } + } + + private static String getArmoredShortInvite(RSId rsId, ByteArrayOutputStream out) throws IOException + { + addPacket(ShortInviteTags.SSLID, rsId.getLocationId(), out); + addPacket(ShortInviteTags.NAME, rsId.getName().getBytes(), out); + addPacket(ShortInviteTags.PGP_FINGERPRINT, rsId.getPgpFingerprint(), out); + if (rsId.isHiddenNode()) + { + addPacket(ShortInviteTags.HIDDEN_LOCATOR, rsId.getHiddenNodeAddress().getAddressAsBytes().orElseThrow(), out); + } + else if (rsId.hasDnsName()) + { + addPacket(ShortInviteTags.DNS_LOCATOR, rsId.getDnsName().getAddressAsBytes().orElseThrow(), out); + } + else if (rsId.hasExternalIp()) + { + addPacket(ShortInviteTags.EXT4_LOCATOR, ShortInviteQuirks.swapBytes(rsId.getExternalIp().getAddressAsBytes().orElseThrow()), out); + } + else if (rsId.hasLocators()) + { + // XXX: use ONE most recently known locator. I still think the url scheme is a waste + } + addCrcPacket(ShortInviteTags.CHECKSUM, out); + + return wrapWithBase64(out.toByteArray(), WrapMode.CONTINUOUS); + } + + private static String getArmoredCertificate(RSId rsId, ByteArrayOutputStream out) throws IOException + { + addPacket(RSCertificateTags.VERSION, new byte[]{RSCertificate.VERSION_06}, out); + if (rsId.hasPgpPublicKey()) + { + addPacket(RSCertificateTags.PGP, rsId.getPgpPublicKey().getEncoded(), out); + } + + if (rsId.hasLocationInfo()) + { + if (rsId.isHiddenNode()) + { + addPacket(RSCertificateTags.HIDDEN_NODE, rsId.getHiddenNodeAddress().getAddressAsBytes().orElseThrow(), out); + } + else + { + if (rsId.hasExternalIp()) + { + addPacket(RSCertificateTags.EXTERNAL_IP_AND_PORT, rsId.getExternalIp().getAddressAsBytes().orElseThrow(), out); + } + if (rsId.hasInternalIp()) + { + addPacket(RSCertificateTags.INTERNAL_IP_AND_PORT, rsId.getInternalIp().getAddressAsBytes().orElseThrow(), out); + } + //if (rsId.hasDnsName()) + //{ + //addPacket(DNS, rsId.getDnsName().getBytes(), out); + //} + } + + if (rsId.hasName()) + { + addPacket(RSCertificateTags.NAME, rsId.getName().getBytes(), out); + } + addPacket(RSCertificateTags.SSLID, rsId.getLocationId(), out); + + if (rsId.hasLocators()) + { + for (String locator : rsId.getLocators()) + { + addPacket(RSCertificateTags.EXTRA_LOCATOR, locator.getBytes(), out); + } + } + } + addCrcPacket(RSCertificateTags.CHECKSUM, out); + + return wrapWithBase64(out.toByteArray(), WrapMode.SLICED); + } + + private static void addPacket(int pTag, Identifier identifier, OutputStream out) throws IOException + { + addPacket(pTag, identifier.getBytes(), out); + } + + private static void addPacket(int pTag, byte[] data, OutputStream out) throws IOException + { + if (data != null) + { + // This is like PGP packets, see https://tools.ietf.org/html/rfc4880 + out.write(pTag); + if (data.length < 192) // size is coded in one byte + { + // one byte + out.write(data.length); + } + else if (data.length < 8384) // size is coded in 2 bytes + { + int octet2 = (data.length - 192) & 0xff; + out.write(((data.length - 192 - octet2) >> 8) + 192); + out.write(octet2); + } + else + { + // We don't support more as it makes little sense to have an oversized certificate + throw new IllegalArgumentException("Packet data size too big: " + data.length); + } + out.write(data); + } + else + { + log.warn("Trying to write certificate tag {} with empty data. Skipping...", pTag); + } + } + + private static void addCrcPacket(int pTag, ByteArrayOutputStream out) throws IOException + { + byte[] data = out.toByteArray(); + + int crc = RSIdCrc.calculate24bitsCrc(data, data.length); + + // Perform byte swapping + var le = new byte[3]; + le[0] = (byte) (crc & 0xff); + le[1] = (byte) ((crc >> 8) & 0xff); + le[2] = (byte) ((crc >> 16) & 0xff); + + addPacket(pTag, le, out); + } + + private static String wrapWithBase64(byte[] data, WrapMode wrapMode) + { + byte[] base64 = Base64.getEncoder().encode(data); + + try (var out = new ByteArrayOutputStream()) + { + for (var i = 0; i < base64.length; i++) + { + out.write(base64[i]); + + if (wrapMode == WrapMode.SLICED && i % 64 == 64 - 1) + { + out.write('\n'); + } + } + return out.toString(StandardCharsets.US_ASCII); + } + catch (IOException e) + { + throw new IllegalStateException(e); + } + } +} diff --git a/app/src/main/java/io/xeres/app/crypto/rsid/RSIdCrc.java b/app/src/main/java/io/xeres/app/crypto/rsid/RSIdCrc.java new file mode 100644 index 000000000..847cb0697 --- /dev/null +++ b/app/src/main/java/io/xeres/app/crypto/rsid/RSIdCrc.java @@ -0,0 +1,47 @@ +/* + * Copyright (c) 2019-2020 by David Gerber - https://zapek.com + * + * This file is part of Xeres. + * + * Xeres is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Xeres is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Xeres. If not, see . + */ + +package io.xeres.app.crypto.rsid; + +public final class RSIdCrc +{ + private RSIdCrc() + { + throw new UnsupportedOperationException("Utility class"); + } + + public static int calculate24bitsCrc(byte[] data, int length) + { + var crc = 0xb704ce; + + for (var i = 0; i < length; i++) + { + crc ^= data[i] << 16; + for (var j = 0; j < 8; j++) + { + crc <<= 1; + if ((crc & 0x1000000) != 0) + { + crc ^= 0x1864cfb; + } + } + } + return crc & 0xffffff; + } +} diff --git a/app/src/main/java/io/xeres/app/crypto/rsid/RSSerialVersion.java b/app/src/main/java/io/xeres/app/crypto/rsid/RSSerialVersion.java new file mode 100644 index 000000000..b74dd66ff --- /dev/null +++ b/app/src/main/java/io/xeres/app/crypto/rsid/RSSerialVersion.java @@ -0,0 +1,66 @@ +/* + * Copyright (c) 2019-2020 by David Gerber - https://zapek.com + * + * This file is part of Xeres. + * + * Xeres is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Xeres is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Xeres. If not, see . + */ + +package io.xeres.app.crypto.rsid; + +import java.math.BigInteger; + +public enum RSSerialVersion +{ + V06_0000("60000", "Retroshare 0.6.4 or earlier"), // RS 0.6.4 and earlier, before November 2017 (note that the version which is in the cert's serial number can be random) + V06_0001("60001", "Retroshare 0.6.5"), // RS 0.6.5 after November 2017 + V07_0001("70001", "Retroshare 0.6.6"); // RS 0.6.6 + + private final String versionString; + private final String description; + + RSSerialVersion(String versionString, String description) + { + this.versionString = versionString; + this.description = description; + } + + public BigInteger serialNumber() + { + return new BigInteger(versionString, 16); + } + + public String versionString() + { + return versionString; + } + + @Override + public String toString() + { + return description + " (" + versionString + ")"; + } + + public static RSSerialVersion getFromSerialNumber(BigInteger serialNumber) + { + for (RSSerialVersion value : values()) + { + if (value.serialNumber().equals(serialNumber)) + { + return value; + } + } + return V06_0000; // old versions used a random serial number + } +} diff --git a/app/src/main/java/io/xeres/app/crypto/rsid/certificate/RSCertificate.java b/app/src/main/java/io/xeres/app/crypto/rsid/certificate/RSCertificate.java new file mode 100644 index 000000000..30e7c40f0 --- /dev/null +++ b/app/src/main/java/io/xeres/app/crypto/rsid/certificate/RSCertificate.java @@ -0,0 +1,345 @@ +/* + * Copyright (c) 2019-2021 by David Gerber - https://zapek.com + * + * This file is part of Xeres. + * + * Xeres is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Xeres is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Xeres. If not, see . + */ + +package io.xeres.app.crypto.rsid.certificate; + +import io.xeres.app.crypto.pgp.PGP; +import io.xeres.app.crypto.rsid.RSId; +import io.xeres.app.crypto.rsid.RSIdCrc; +import io.xeres.app.net.protocol.PeerAddress; +import io.xeres.common.id.LocationId; +import org.bouncycastle.openpgp.PGPPublicKey; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.security.InvalidKeyException; +import java.security.cert.CertificateParsingException; +import java.util.Base64; +import java.util.HashSet; +import java.util.Set; + +public final class RSCertificate extends RSId +{ + private static final Logger log = LoggerFactory.getLogger(RSCertificate.class); + + public static final int VERSION_06 = 6; + + private PGPPublicKey pgpPublicKey; + + private String name; + private LocationId locationIdentifier; + + // Note that dnsName is not supported because it doesn't have a port (XXX: it could be supported by using the one from externalIp though) + private PeerAddress hiddenNodeAddress; + private PeerAddress internalIp; + private PeerAddress externalIp; + private Set locators = new HashSet<>(); + + public RSCertificate() + { + } + + public static RSCertificate parseRSCertificate(String data) throws CertificateParsingException + { + try + { + var cert = new RSCertificate(); + byte[] certBytes = Base64.getDecoder().decode(cleanupInput(data.getBytes())); + int checksum = RSIdCrc.calculate24bitsCrc(certBytes, certBytes.length - 5); // ignore the checksum PTAG which is 5 bytes in total and at the end + var in = new ByteArrayInputStream(certBytes); + var version = 0; + Boolean checksumPassed = null; + + while (in.available() > 0) + { + int ptag = in.read(); + int size = getPacketSize(in); + if (size == 0) + { + continue; // seen in the wild, just skip them + } + var buf = new byte[size]; + if (in.readNBytes(buf, 0, size) != size) + { + throw new IllegalArgumentException("Packet " + ptag + " is shorter than its advertised size"); + } + + switch (ptag) + { + case RSCertificateTags.VERSION: + version = buf[0]; + break; + + case RSCertificateTags.PGP: + cert.setPgpPublicKey(buf); + break; + + case RSCertificateTags.NAME: + cert.setLocationName(buf); + break; + + case RSCertificateTags.SSLID: + cert.setLocationId(new LocationId(buf)); + break; + + case RSCertificateTags.DNS: + cert.setDnsName(buf); + break; + + case RSCertificateTags.HIDDEN_NODE: + cert.setHiddenNodeAddress(buf); + break; + + case RSCertificateTags.INTERNAL_IP_AND_PORT: + cert.setInternalIp(buf); + break; + + case RSCertificateTags.EXTERNAL_IP_AND_PORT: + cert.setExternalIp(buf); + break; + + case RSCertificateTags.CHECKSUM: + if (buf.length != 3) + { + throw new IllegalArgumentException("Checksum corrupted"); + } + checksumPassed = checksum == (Byte.toUnsignedInt(buf[2]) << 16 | Byte.toUnsignedInt(buf[1]) << 8 | Byte.toUnsignedInt(buf[0])); // little endian + break; + + case RSCertificateTags.EXTRA_LOCATOR: + // XXX: insert the URLs (I probably need a RsUrl object... + break; + + default: + RSCertificate.log.warn("Unhandled tag {}, ignoring.", ptag); + break; + } + } + + if (version == 0) + { + throw new IllegalArgumentException("Missing certificate version"); + } + else if (version != RSCertificate.VERSION_06) + { + throw new IllegalArgumentException("Wrong certificate version: " + version); + } + + if (checksumPassed == null) + { + throw new IllegalArgumentException("Missing checksum packet"); + } + else if (Boolean.FALSE.equals(checksumPassed)) + { + throw new IllegalArgumentException("Wrong checksum"); + } + return cert; + } + catch (IllegalArgumentException | IOException e) + { + throw new CertificateParsingException("Parse error: " + e.getMessage(), e); + } + } + + public void setInternalIp(byte[] data) + { + internalIp = PeerAddress.fromByteArray(data); + } + + public void setInternalIp(String ipAndPort) + { + internalIp = PeerAddress.fromIpAndPort(ipAndPort); + } + + public void setExternalIp(byte[] data) + { + externalIp = PeerAddress.fromByteArray(data); + } + + public void setExternalIp(String ipAndPort) + { + externalIp = PeerAddress.fromIpAndPort(ipAndPort); + } + + @Override + public boolean hasInternalIp() + { + return internalIp != null && internalIp.isValid(); + } + + @Override + public PeerAddress getInternalIp() + { + return internalIp; + } + + @Override + public boolean hasExternalIp() + { + return externalIp != null && externalIp.isValid(); + } + + @Override + public PeerAddress getExternalIp() + { + return externalIp; + } + + @Override + public boolean hasPgpPublicKey() + { + return pgpPublicKey != null; + } + + @Override + public PGPPublicKey getPgpPublicKey() + { + return pgpPublicKey; + } + + public void setPgpPublicKey(byte[] data) throws CertificateParsingException + { + try + { + this.pgpPublicKey = PGP.getPGPPublicKey(data); + } + catch (InvalidKeyException e) + { + throw new CertificateParsingException("Error in public PGP key: " + e.getMessage(), e); + } + } + + @Override + public byte[] getPgpFingerprint() + { + return pgpPublicKey.getFingerprint(); + } + + @Override + public boolean hasName() + { + return name != null; + } + + @Override + public String getName() + { + return name; + } + + public void setName(String name) + { + this.name = name; + } + + public void setLocationName(byte[] name) + { + // XXX: we need a maximum location name. Find out what is RS' limit + this.name = new String(name, StandardCharsets.UTF_8); + } + + @Override + public boolean hasLocationInfo() + { + return locationIdentifier != null; + } + + @Override + public LocationId getLocationId() + { + return locationIdentifier; + } + + public void setLocationId(LocationId locationId) + { + this.locationIdentifier = locationId; + } + + @Override + public boolean hasDnsName() + { + return false; + } + + @Override + public PeerAddress getDnsName() + { + return null; + } + + public void setDnsName(String dnsName) + { + // do nothing, we don't support those anymore + } + + public void setDnsName(byte[] dnsName) + { + // ditto + } + + @Override + public boolean isHiddenNode() + { + return hiddenNodeAddress != null; + } + + @Override + public PeerAddress getHiddenNodeAddress() + { + return hiddenNodeAddress; + } + + public void setHiddenNodeAddress(String hiddenNodeAddress) + { + this.hiddenNodeAddress = PeerAddress.fromHidden(hiddenNodeAddress); + } + + public void setHiddenNodeAddress(byte[] hiddenNodeAddress) + { + if (hiddenNodeAddress != null && hiddenNodeAddress.length >= 5 && hiddenNodeAddress.length <= 255) + { + setHiddenNodeAddress(new String(hiddenNodeAddress, StandardCharsets.US_ASCII)); + } + else + { + this.hiddenNodeAddress = PeerAddress.fromInvalid(); + } + } + + @Override + public boolean hasLocators() + { + return locators != null && !locators.isEmpty(); + } + + @Override + public Set getLocators() + { + return locators; + } + + public void setLocators(Set locators) + { + // XXX: make sure none of them is null + this.locators = locators; + } +} diff --git a/app/src/main/java/io/xeres/app/crypto/rsid/certificate/RSCertificateTags.java b/app/src/main/java/io/xeres/app/crypto/rsid/certificate/RSCertificateTags.java new file mode 100644 index 000000000..b5478fa8b --- /dev/null +++ b/app/src/main/java/io/xeres/app/crypto/rsid/certificate/RSCertificateTags.java @@ -0,0 +1,39 @@ +/* + * Copyright (c) 2019-2020 by David Gerber - https://zapek.com + * + * This file is part of Xeres. + * + * Xeres is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Xeres is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Xeres. If not, see . + */ + +package io.xeres.app.crypto.rsid.certificate; + +public final class RSCertificateTags +{ + public static final int PGP = 1; + public static final int EXTERNAL_IP_AND_PORT = 2; + public static final int INTERNAL_IP_AND_PORT = 3; + public static final int DNS = 4; + public static final int SSLID = 5; + public static final int NAME = 6; + public static final int CHECKSUM = 7; + public static final int HIDDEN_NODE = 8; + public static final int VERSION = 9; + public static final int EXTRA_LOCATOR = 10; + + private RSCertificateTags() + { + throw new UnsupportedOperationException("Utility class"); + } +} diff --git a/app/src/main/java/io/xeres/app/crypto/rsid/shortinvite/ShortInvite.java b/app/src/main/java/io/xeres/app/crypto/rsid/shortinvite/ShortInvite.java new file mode 100644 index 000000000..d4a86c516 --- /dev/null +++ b/app/src/main/java/io/xeres/app/crypto/rsid/shortinvite/ShortInvite.java @@ -0,0 +1,312 @@ +/* + * Copyright (c) 2019-2021 by David Gerber - https://zapek.com + * + * This file is part of Xeres. + * + * Xeres is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Xeres is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Xeres. If not, see . + */ + +package io.xeres.app.crypto.rsid.shortinvite; + +import io.xeres.app.crypto.rsid.RSId; +import io.xeres.app.crypto.rsid.RSIdCrc; +import io.xeres.app.net.protocol.PeerAddress; +import io.xeres.common.id.LocationId; +import org.bouncycastle.openpgp.PGPPublicKey; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.security.cert.CertificateParsingException; +import java.util.Arrays; +import java.util.Base64; +import java.util.HashSet; +import java.util.Set; + +public class ShortInvite extends RSId +{ + private static final Logger log = LoggerFactory.getLogger(ShortInvite.class); + + private String name; + private LocationId locationId; + + private byte[] pgpFingerprint; + private PeerAddress hiddenLocator; + private PeerAddress ext4Locator; + private PeerAddress loc4Locator; + private PeerAddress hostnameLocator; + private final Set locators = new HashSet<>(); + + public ShortInvite() + { + } + + public static ShortInvite parseShortInvite(String data) throws CertificateParsingException + { + try + { + var shortInvite = new ShortInvite(); + byte[] shortInviteBytes = Base64.getDecoder().decode(cleanupInput(data.getBytes())); + int checksum = RSIdCrc.calculate24bitsCrc(shortInviteBytes, shortInviteBytes.length - 5); // ignore the checksum PTAG which is 5 bytes in total and at the end + var in = new ByteArrayInputStream(shortInviteBytes); + Boolean checksumPassed = null; + + while (in.available() > 0) + { + int ptag = in.read(); + int size = getPacketSize(in); + if (size == 0) + { + continue; // not seen in the wild yet but just skip them in any case + } + var buf = new byte[size]; + if (in.readNBytes(buf, 0, size) != size) + { + throw new IllegalArgumentException("Packet " + ptag + " is shorter than its advertised size"); + } + + switch (ptag) + { + case ShortInviteTags.PGP_FINGERPRINT: + shortInvite.setPgpFingerprint(buf); + break; + + case ShortInviteTags.NAME: + shortInvite.setName(buf); + break; + + case ShortInviteTags.SSLID: + shortInvite.setLocationId(new LocationId(buf)); + break; + + case ShortInviteTags.DNS_LOCATOR: + shortInvite.setDnsName(buf); + break; + + case ShortInviteTags.HIDDEN_LOCATOR: + shortInvite.setHiddenNodeAddress(buf); + break; + + case ShortInviteTags.EXT4_LOCATOR: + shortInvite.setExt4Locator(buf); + break; + + case ShortInviteTags.LOC4_LOCATOR: + shortInvite.setLoc4Locator(buf); + break; + + case ShortInviteTags.CHECKSUM: + if (buf.length != 3) + { + throw new IllegalArgumentException("Checksum corrupted"); + } + checksumPassed = checksum == (Byte.toUnsignedInt(buf[2]) << 16 | Byte.toUnsignedInt(buf[1]) << 8 | Byte.toUnsignedInt(buf[0])); // little endian + break; + + case ShortInviteTags.LOCATOR: + // XXX: handle the URLs... + break; + + default: + ShortInvite.log.warn("Unhandled tag {}, ignoring.", ptag); + break; + + } + } + + if (checksumPassed == null) + { + throw new IllegalArgumentException("Missing checksum packet"); + } + else if (Boolean.FALSE.equals(checksumPassed)) + { + throw new IllegalArgumentException("Wrong checksum"); + } + return shortInvite; + } + catch (IllegalArgumentException | IOException e) + { + throw new CertificateParsingException("Parse error: " + e.getMessage(), e); + } + } + + public void setExt4Locator(byte[] data) + { + ext4Locator = PeerAddress.fromByteArray(ShortInviteQuirks.swapBytes(data)); + } + + public void setExt4Locator(String ipAndPort) + { + ext4Locator = PeerAddress.fromIpAndPort(ipAndPort); + } + + private void setLoc4Locator(byte[] data) + { + loc4Locator = PeerAddress.fromByteArray(ShortInviteQuirks.swapBytes(data)); + } + + public void setLoc4Locator(String ipAndPort) + { + loc4Locator = PeerAddress.fromIpAndPort(ipAndPort); + } + + @Override + public boolean hasInternalIp() + { + return loc4Locator != null && loc4Locator.isValid(); + } + + @Override + public PeerAddress getInternalIp() + { + return loc4Locator; + } + + @Override + public boolean hasExternalIp() + { + return ext4Locator != null && ext4Locator.isValid(); + } + + @Override + public PeerAddress getExternalIp() + { + return ext4Locator; + } + + @Override + public boolean hasPgpPublicKey() + { + return false; + } + + @Override + public PGPPublicKey getPgpPublicKey() + { + return null; + } + + public void setPgpFingerprint(byte[] pgpFingerprint) + { + this.pgpFingerprint = pgpFingerprint; + } + + @Override + public byte[] getPgpFingerprint() + { + return pgpFingerprint; + } + + @Override + public boolean hasName() + { + return name != null; + } + + @Override + public String getName() + { + return name; + } + + public void setName(byte[] name) + { + // XXX: need maximum name size + this.name = new String(name, StandardCharsets.UTF_8); + } + + @Override + public boolean hasLocationInfo() + { + return locationId != null; + } + + @Override + public LocationId getLocationId() + { + return locationId; + } + + public void setLocationId(LocationId locationId) + { + this.locationId = locationId; + } + + @Override + public boolean hasDnsName() + { + return false; + } + + @Override + public PeerAddress getDnsName() + { + return hostnameLocator; + } + + private void setDnsName(byte[] portAndDns) + { + if (portAndDns == null || portAndDns.length <= 3 || portAndDns.length > 255) + { + throw new IllegalArgumentException("DNS name format is wrong"); + } + + int port = Byte.toUnsignedInt(portAndDns[0]) << 8 | Byte.toUnsignedInt(portAndDns[1]); + var hostname = new String(Arrays.copyOfRange(portAndDns, 2, portAndDns.length), StandardCharsets.US_ASCII); + hostnameLocator = PeerAddress.fromHostname(hostname, port); + } + + @Override + public boolean isHiddenNode() + { + return hiddenLocator != null; + } + + @Override + public PeerAddress getHiddenNodeAddress() + { + return hiddenLocator; + } + + private void setHiddenNodeAddress(String hiddenNodeAddress) + { + this.hiddenLocator = PeerAddress.fromHidden(hiddenNodeAddress); + } + + private void setHiddenNodeAddress(byte[] hiddenNodeAddress) + { + if (hiddenNodeAddress != null && hiddenNodeAddress.length >= 5 && hiddenNodeAddress.length <= 255) + { + setHiddenNodeAddress(new String(hiddenNodeAddress, StandardCharsets.US_ASCII)); + } + else + { + this.hiddenLocator = PeerAddress.fromInvalid(); + } + } + + @Override + public boolean hasLocators() + { + return !locators.isEmpty(); + } + + @Override + public Set getLocators() + { + return locators; + } +} diff --git a/app/src/main/java/io/xeres/app/crypto/rsid/shortinvite/ShortInviteQuirks.java b/app/src/main/java/io/xeres/app/crypto/rsid/shortinvite/ShortInviteQuirks.java new file mode 100644 index 000000000..87a60f2ff --- /dev/null +++ b/app/src/main/java/io/xeres/app/crypto/rsid/shortinvite/ShortInviteQuirks.java @@ -0,0 +1,54 @@ +/* + * Copyright (c) 2019-2020 by David Gerber - https://zapek.com + * + * This file is part of Xeres. + * + * Xeres is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Xeres is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Xeres. If not, see . + */ + +package io.xeres.app.crypto.rsid.shortinvite; + +public final class ShortInviteQuirks +{ + private ShortInviteQuirks() + { + throw new UnsupportedOperationException("Utility class"); + } + + /** + * Retroshare puts IP addresses in big-endian in certificates, but when it comes + * to short invites, a mistake was made and, while the port is in big-endian, the + * IP address is not. Since the mistake is done on output and input, it works fine + * within Retroshare so a workaround has to be implemented here. + * + * @param data the IP address + port + * @return the IP address in swapped endian + port left alone + */ + public static byte[] swapBytes(byte[] data) + { + if (data == null || data.length != 6) + { + return data; // don't touch anything, input is bad + } + var bytes = new byte[6]; + bytes[0] = data[3]; + bytes[1] = data[2]; + bytes[2] = data[1]; + bytes[3] = data[0]; + bytes[4] = data[4]; + bytes[5] = data[5]; + + return bytes; + } +} diff --git a/app/src/main/java/io/xeres/app/crypto/rsid/shortinvite/ShortInviteTags.java b/app/src/main/java/io/xeres/app/crypto/rsid/shortinvite/ShortInviteTags.java new file mode 100644 index 000000000..eabf722b6 --- /dev/null +++ b/app/src/main/java/io/xeres/app/crypto/rsid/shortinvite/ShortInviteTags.java @@ -0,0 +1,38 @@ +/* + * Copyright (c) 2019-2021 by David Gerber - https://zapek.com + * + * This file is part of Xeres. + * + * Xeres is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Xeres is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Xeres. If not, see . + */ + +package io.xeres.app.crypto.rsid.shortinvite; + +public final class ShortInviteTags +{ + public static final int SSLID = 0x0; + public static final int NAME = 0x1; + public static final int LOCATOR = 0x2; + public static final int PGP_FINGERPRINT = 0x3; + public static final int CHECKSUM = 0x4; + public static final int HIDDEN_LOCATOR = 0x90; + public static final int DNS_LOCATOR = 0x91; + public static final int EXT4_LOCATOR = 0x92; + public static final int LOC4_LOCATOR = 0x93; + + private ShortInviteTags() + { + throw new UnsupportedOperationException("Utility class"); + } +} diff --git a/app/src/main/java/io/xeres/app/crypto/scramble/ScrambledString.java b/app/src/main/java/io/xeres/app/crypto/scramble/ScrambledString.java new file mode 100644 index 000000000..09fe29203 --- /dev/null +++ b/app/src/main/java/io/xeres/app/crypto/scramble/ScrambledString.java @@ -0,0 +1,306 @@ +/* + * Copyright (c) 2019-2021 by David Gerber - https://zapek.com + * + * This file is part of Xeres. + * + * Xeres is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Xeres is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Xeres. If not, see . + */ + +package io.xeres.app.crypto.scramble; + +import org.apache.tomcat.util.codec.binary.Base64; + +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; +import java.security.SecureRandom; +import java.util.Arrays; + +/** + * String obfuscator. This class is used to store a password in memory (for example after asking + * the user for a password). Instead of storing the password in clear text, it is stored in a + * scrambled form which makes it harder to recover if it ever ends up in a memory dump. + *

+ * Once the password has been handled, it is recommended to call dispose() to clear it. + *

+ * Please be wary that it is still possible to recover the password if the attacker knows the + * memory layout and what he's looking for but at least the password won't show up for a simple + * string search. + *

+ * Note: the interface is similar to Sun's GuardedString. + */ +public class ScrambledString +{ + /** + * Callback to access the clear text of the secure string. + * If possible, prefer the use of verifyBase64SHA256Hash() + * which avoids unscrambling the secure string. + */ + public interface Accessor + { + void access(char[] clearChars); + } + + private boolean disposed; + private byte[] padBytes; + private byte[] scrambledBytes; + private String hash; + private final MessageDigest digest; + private SecureRandom random; + + /** + * Create an empty scrambled string. + */ + public ScrambledString() + { + this(new char[0]); + } + + /** + * Create a scrambled string from cleartext characters. + * The caller is responsible for clearing the cleartext characters itself. + * + * @param clearChars the cleartext characters + */ + public ScrambledString(char[] clearChars) + { + try + { + digest = MessageDigest.getInstance("SHA-256"); + } + catch (NoSuchAlgorithmException e) + { + throw new IllegalStateException("No SHA-256 available on this system"); + } + scrambleChars(clearChars); + } + + /** + * Allow access to the cleartext characters. + *

+ * They're only available during the call and are automatically + * cleared afterwards. + * + * @param accessor the Accessor callback + * @throws IllegalStateException the string has been disposed already + */ + public void access(Accessor accessor) + { + checkNotDisposed(); + char[] clearText = null; + try + { + clearText = unscrambleChars(); + accessor.access(clearText); + } + finally + { + clear(clearText); + } + } + + /** + * Append a single char to the scrambled string. + * + * @param c the character to append + * @throws IllegalStateException if the string has been disposed already + */ + public void appendChar(char c) + { + checkNotDisposed(); + char[] oldArray = null; + char[] newArray = null; + + try + { + oldArray = unscrambleChars(); + newArray = new char[oldArray.length + 1]; + System.arraycopy(oldArray, 0, newArray, 0, oldArray.length); + newArray[newArray.length - 1] = c; + scrambleChars(newArray); + } + finally + { + clear(oldArray); + clear(newArray); + } + + } + + /** + * Clear the scrambled string. Should be called as soon as we're done with the + * string. Note that the string cannot be reused afterwards and a new one must be + * created. + */ + public void dispose() + { + clear(scrambledBytes); + disposed = true; + } + + /** + * Check that the base64 encoded SHA-256 hash of the original clear text + * matches the given value. + * + * @param hash the base64 encoded SHA-256 hash of the clear text + * @return true of the hash matches the original clear text's hash + */ + public boolean verifyBase64SHA256Hash(String hash) + { + checkNotDisposed(); + return this.hash.equals(hash); + } + + /** + * Get the base64 encoded SHA-256 hash of the original clear text. Useful to + * store in a database for example. + * + * @return the base64 encoded SHA-256 hash of the original clear text + */ + public String getBase64SHA256Hash() + { + checkNotDisposed(); + return this.hash; + } + + private void regeneratePad(int length) + { + clear(padBytes); + padBytes = new byte[length]; + getSecureRandom().nextBytes(padBytes); + } + + private SecureRandom getSecureRandom() + { + if (random == null) + { + random = new SecureRandom(); + } + return random; + } + + private void scrambleBytes(byte[] bytes) + { + regeneratePad(bytes.length); + var newBytes = new byte[bytes.length]; + + for (var i = 0; i < bytes.length; i++) + { + newBytes[i] = (byte) (padBytes[i] ^ bytes[i]); + } + + clear(scrambledBytes); + scrambledBytes = newBytes; + hash = Base64.encodeBase64String(digest.digest(bytes)); + } + + private byte[] unscrambleBytes() + { + var unscrambledBytes = new byte[scrambledBytes.length]; + for (var i = 0; i < scrambledBytes.length; i++) + { + unscrambledBytes[i] = (byte) (padBytes[i] ^ scrambledBytes[i]); + } + return unscrambledBytes; + } + + private void scrambleChars(char[] chars) + { + byte[] clearBytes = null; + try + { + clearBytes = charsToBytes(chars); + scrambleBytes(clearBytes); + } + finally + { + clear(clearBytes); + } + } + + private char[] unscrambleChars() + { + byte[] unscrambledBytes = unscrambleBytes(); + char[] unscrambledChars = bytesToChars(unscrambledBytes); + clear(unscrambledBytes); + return unscrambledChars; + } + + private byte[] charsToBytes(char[] chars) + { + var bytes = new byte[chars.length]; + for (var i = 0; i < chars.length; i++) + { + bytes[i] = (byte) chars[i]; + } + return bytes; + } + + private char[] bytesToChars(byte[] bytes) + { + var chars = new char[bytes.length]; + for (var i = 0; i < bytes.length; i++) + { + chars[i] = (char) bytes[i]; + } + return chars; + } + + private void clear(byte[] bytes) + { + if (bytes == null) + { + return; + } + Arrays.fill(bytes, (byte) 0); + } + + private void clear(char[] chars) + { + if (chars == null) + { + return; + } + Arrays.fill(chars, (char) 0); + } + + private void checkNotDisposed() + { + if (disposed) + { + throw new IllegalStateException("String is disposed already"); + } + } + + @Override + public boolean equals(Object obj) + { + if (obj instanceof ScrambledString other) + { + return hash.equals(other.hash); + } + return false; + } + + @Override + public int hashCode() + { + return hash.hashCode(); + } + + @Override + public String toString() + { + return disposed ? "" : "[SCRAMBLED]"; + } +} diff --git a/app/src/main/java/io/xeres/app/crypto/x509/X509.java b/app/src/main/java/io/xeres/app/crypto/x509/X509.java new file mode 100644 index 000000000..a58db56a1 --- /dev/null +++ b/app/src/main/java/io/xeres/app/crypto/x509/X509.java @@ -0,0 +1,138 @@ +/* + * Copyright (c) 2019-2020 by David Gerber - https://zapek.com + * + * This file is part of Xeres. + * + * Xeres is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Xeres is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Xeres. If not, see . + */ + +package io.xeres.app.crypto.x509; + +import io.xeres.app.crypto.pgp.PGPSigner; +import io.xeres.app.crypto.rsid.RSSerialVersion; +import io.xeres.common.id.LocationId; +import org.bouncycastle.asn1.x500.X500Name; +import org.bouncycastle.asn1.x509.SubjectPublicKeyInfo; +import org.bouncycastle.cert.X509v1CertificateBuilder; +import org.bouncycastle.openpgp.PGPSecretKey; + +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.math.BigInteger; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; +import java.security.PublicKey; +import java.security.cert.CertificateException; +import java.security.cert.CertificateFactory; +import java.security.cert.X509Certificate; +import java.util.Date; +import java.util.Optional; + +/** + * Implements all X509 certificate functions. Used to create a SSL certificate for the location. + */ +public final class X509 +{ + private static final String CERTIFICATE_TYPE = "X.509"; + + private X509() + { + throw new UnsupportedOperationException("Utility class"); + } + + /** + * Generates a certificate. + * + * @param pgpSecretKey a PGP secret key + * @param rsaPublicKey an RSA public key + * @param issuer the issuer + * @param subject the subject + * @param dateOfIssue date of certificate validity + * @param dateOfExpiry date of certificate expiration + * @param serial serial number + * @return a X509Certificate + * @throws IOException I/O error + * @throws CertificateException Certificate error + */ + public static X509Certificate generateCertificate(PGPSecretKey pgpSecretKey, PublicKey rsaPublicKey, String issuer, String subject, Date dateOfIssue, Date dateOfExpiry, BigInteger serial) throws IOException, CertificateException + { + var certificateBuilder = new X509v1CertificateBuilder( + new X500Name(issuer), + serial, + dateOfIssue, + dateOfExpiry, + new X500Name(subject), + SubjectPublicKeyInfo.getInstance(rsaPublicKey.getEncoded()) + ); + + var pgpSigner = new PGPSigner(pgpSecretKey); + byte[] certificateBytes = certificateBuilder.build(pgpSigner).getEncoded(); + + return (X509Certificate) CertificateFactory.getInstance(CERTIFICATE_TYPE).generateCertificate(new ByteArrayInputStream(certificateBytes)); + } + + /** + * Gets the certificate from its encoded form. + * @param data a byte array with the encoded certificate + * @return a X509 certificate + * @throws CertificateException parse error + */ + public static X509Certificate getCertificate(byte[] data) throws CertificateException + { + return (X509Certificate) CertificateFactory.getInstance(CERTIFICATE_TYPE).generateCertificate(new ByteArrayInputStream(data)); + } + + /** + * Gets the SSL ID of the certificate. + * + * @param certificate the X509 certificate + * @return the ID that can be used as SSL ID + */ + public static LocationId getLocationId(X509Certificate certificate) throws CertificateException + { + try + { + BigInteger serialNumber = Optional.ofNullable(certificate.getSerialNumber()).orElseThrow(() -> new CertificateException("Missing serial number")); + + var out = new byte[LocationId.LENGTH]; + + // There are several certificate versions + if (serialNumber.equals(RSSerialVersion.V07_0001.serialNumber())) + { + // RS 0.6.6. ID is SHA-256 of signature (16 first bytes) + var md = MessageDigest.getInstance("SHA-256"); + md.update(certificate.getSignature()); + System.arraycopy(md.digest(), 0, out, 0, out.length); + } + else if (serialNumber.equals(RSSerialVersion.V06_0001.serialNumber())) + { + // RS 0.6.5 after November 2017, ID is SHA-1 of signature (16 first bytes) + var md = MessageDigest.getInstance("SHA-1"); + md.update(certificate.getSignature()); + System.arraycopy(md.digest(), 0, out, 0, out.length); + } + else + { + // The serial number here is either "60000" or a totally random string. + // RS < November 2017. ID is the last 16 bytes of the signature. + System.arraycopy(certificate.getSignature(), certificate.getSignature().length - out.length, out, 0, out.length); + } + return new LocationId(out); + } + catch (NoSuchAlgorithmException e) + { + throw new IllegalStateException("Missing algorithm in JCE provider: " + e.getMessage(), e); + } + } +} diff --git a/app/src/main/java/io/xeres/app/database/DatabaseSession.java b/app/src/main/java/io/xeres/app/database/DatabaseSession.java new file mode 100644 index 000000000..3d6e6fd88 --- /dev/null +++ b/app/src/main/java/io/xeres/app/database/DatabaseSession.java @@ -0,0 +1,56 @@ +/* + * Copyright (c) 2019-2021 by David Gerber - https://zapek.com + * + * This file is part of Xeres. + * + * Xeres is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Xeres is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Xeres. If not, see . + */ + +package io.xeres.app.database; + +/** + * Allows to use transactions from outside spring controllers, while still allowing the controller + * to call such methods directly. For example: + *

+ *     @Autowired
+ *     private DatabaseSessionManager databaseSessionManager;
+ *
+ *     ...
+ *
+ *     try (var session = new DatabaseSession(databaseSessionManager))
+ *     {
+ *         ... use your JPA entities here ...
+ *     }
+ * 
+ */ +public class DatabaseSession implements AutoCloseable +{ + private final DatabaseSessionManager databaseSessionManager; + private final boolean isBound; + + public DatabaseSession(DatabaseSessionManager databaseSessionManager) + { + this.databaseSessionManager = databaseSessionManager; + this.isBound = databaseSessionManager.bindSession(); + } + + @Override + public void close() + { + if (isBound) + { + databaseSessionManager.unbindSession(); + } + } +} diff --git a/app/src/main/java/io/xeres/app/database/DatabaseSessionManager.java b/app/src/main/java/io/xeres/app/database/DatabaseSessionManager.java new file mode 100644 index 000000000..f09db296d --- /dev/null +++ b/app/src/main/java/io/xeres/app/database/DatabaseSessionManager.java @@ -0,0 +1,56 @@ +/* + * Copyright (c) 2019-2021 by David Gerber - https://zapek.com + * + * This file is part of Xeres. + * + * Xeres is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Xeres is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Xeres. If not, see . + */ + +package io.xeres.app.database; + +import org.springframework.orm.jpa.EntityManagerFactoryUtils; +import org.springframework.orm.jpa.EntityManagerHolder; +import org.springframework.stereotype.Component; +import org.springframework.transaction.support.TransactionSynchronizationManager; + +import javax.persistence.EntityManagerFactory; +import javax.persistence.PersistenceUnit; + +/** + * Allows to use @Transaction from outside Spring Boot threads. Prefer using {@link DatabaseSession} which implements + * an AutoCloseable interface. + */ +@Component +public class DatabaseSessionManager +{ + @PersistenceUnit + private EntityManagerFactory entityManagerFactory; + + public boolean bindSession() + { + if (!TransactionSynchronizationManager.hasResource(entityManagerFactory)) + { + var entityManager = entityManagerFactory.createEntityManager(); + TransactionSynchronizationManager.bindResource(entityManagerFactory, new EntityManagerHolder(entityManager)); + return true; + } + return false; + } + + public void unbindSession() + { + EntityManagerHolder emHolder = (EntityManagerHolder) TransactionSynchronizationManager.unbindResource(entityManagerFactory); + EntityManagerFactoryUtils.closeEntityManager(emHolder.getEntityManager()); + } +} diff --git a/app/src/main/java/io/xeres/app/database/converter/EnumSetConverter.java b/app/src/main/java/io/xeres/app/database/converter/EnumSetConverter.java new file mode 100644 index 000000000..b02f6bf31 --- /dev/null +++ b/app/src/main/java/io/xeres/app/database/converter/EnumSetConverter.java @@ -0,0 +1,62 @@ +/* + * Copyright (c) 2019-2021 by David Gerber - https://zapek.com + * + * This file is part of Xeres. + * + * Xeres is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Xeres is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Xeres. If not, see . + */ + +package io.xeres.app.database.converter; + +import javax.persistence.AttributeConverter; +import javax.persistence.Converter; +import java.util.EnumSet; +import java.util.Set; + +@Converter +public abstract class EnumSetConverter> implements AttributeConverter, Integer> +{ + abstract Class getEnumClass(); + + @Override + public Integer convertToDatabaseColumn(Set enumSet) + { + var value = 0; + + if (enumSet != null) + { + for (Enum anEnum : enumSet) + { + value |= 1 << anEnum.ordinal(); + } + } + return value; + } + + @Override + public Set convertToEntityAttribute(Integer value) + { + var e = getEnumClass(); + + var enumSet = EnumSet.noneOf(e); + for (E enumConstant : e.getEnumConstants()) + { + if ((value & (1 << enumConstant.ordinal())) != 0) + { + enumSet.add(enumConstant); + } + } + return enumSet; + } +} diff --git a/app/src/main/java/io/xeres/app/database/converter/GxsPrivacyFlagsConverter.java b/app/src/main/java/io/xeres/app/database/converter/GxsPrivacyFlagsConverter.java new file mode 100644 index 000000000..cc993ecbf --- /dev/null +++ b/app/src/main/java/io/xeres/app/database/converter/GxsPrivacyFlagsConverter.java @@ -0,0 +1,34 @@ +/* + * Copyright (c) 2019-2021 by David Gerber - https://zapek.com + * + * This file is part of Xeres. + * + * Xeres is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Xeres is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Xeres. If not, see . + */ + +package io.xeres.app.database.converter; + +import io.xeres.app.database.model.gxs.GxsPrivacyFlags; + +import javax.persistence.Converter; + +@Converter +public class GxsPrivacyFlagsConverter extends EnumSetConverter +{ + @Override + Class getEnumClass() + { + return GxsPrivacyFlags.class; + } +} diff --git a/app/src/main/java/io/xeres/app/database/converter/GxsSignatureFlagsConverter.java b/app/src/main/java/io/xeres/app/database/converter/GxsSignatureFlagsConverter.java new file mode 100644 index 000000000..ac8edca26 --- /dev/null +++ b/app/src/main/java/io/xeres/app/database/converter/GxsSignatureFlagsConverter.java @@ -0,0 +1,34 @@ +/* + * Copyright (c) 2019-2021 by David Gerber - https://zapek.com + * + * This file is part of Xeres. + * + * Xeres is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Xeres is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Xeres. If not, see . + */ + +package io.xeres.app.database.converter; + +import io.xeres.app.database.model.gxs.GxsSignatureFlags; + +import javax.persistence.Converter; + +@Converter +public class GxsSignatureFlagsConverter extends EnumSetConverter +{ + @Override + Class getEnumClass() + { + return GxsSignatureFlags.class; + } +} diff --git a/app/src/main/java/io/xeres/app/database/model/chatroom/ChatRoom.java b/app/src/main/java/io/xeres/app/database/model/chatroom/ChatRoom.java new file mode 100644 index 000000000..2885aaaa3 --- /dev/null +++ b/app/src/main/java/io/xeres/app/database/model/chatroom/ChatRoom.java @@ -0,0 +1,151 @@ +/* + * Copyright (c) 2019-2021 by David Gerber - https://zapek.com + * + * This file is part of Xeres. + * + * Xeres is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Xeres is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Xeres. If not, see . + */ + +package io.xeres.app.database.model.chatroom; + +import io.xeres.app.database.model.identity.Identity; +import io.xeres.app.xrs.service.chat.RoomFlags; +import org.apache.commons.lang3.EnumUtils; + +import javax.persistence.*; +import java.util.Set; + +@Table(name = "chatrooms") +@Entity +public class ChatRoom +{ + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private long id; + + private long roomId; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "identity_id") + private Identity identity; + + private String name; + private String topic; + private int flags; + private boolean subscribed; + private boolean joined; + + protected ChatRoom() + { + + } + + protected ChatRoom(long roomId, Identity identity, String name, String topic, int flags) + { + this.roomId = roomId; + this.identity = identity; + this.name = name; + this.topic = topic; + this.flags = flags; + } + + public static ChatRoom createChatRoom(io.xeres.app.xrs.service.chat.ChatRoom serviceChatRoom, Identity identity) + { + return new ChatRoom(serviceChatRoom.getId(), + identity, + serviceChatRoom.getName(), + serviceChatRoom.getTopic(), + (int) EnumUtils.generateBitVector(RoomFlags.class, serviceChatRoom.getRoomFlags())); + } + + public long getId() + { + return id; + } + + public void setId(long id) + { + this.id = id; + } + + public long getRoomId() + { + return roomId; + } + + public void setRoomId(long roomId) + { + this.roomId = roomId; + } + + public Identity getIdentity() + { + return identity; + } + + public void setIdentity(Identity identity) + { + this.identity = identity; + } + + public String getName() + { + return name; + } + + public void setName(String name) + { + this.name = name; + } + + public String getTopic() + { + return topic; + } + + public void setTopic(String topic) + { + this.topic = topic; + } + + public Set getFlags() + { + return EnumUtils.processBitVector(RoomFlags.class, flags); + } + + public void setFlags(Set flags) + { + this.flags = (int) EnumUtils.generateBitVector(RoomFlags.class, flags); + } + + public boolean isSubscribed() + { + return subscribed; + } + + public void setSubscribed(boolean subscribed) + { + this.subscribed = subscribed; + } + + public boolean isJoined() + { + return joined; + } + + public void setJoined(boolean joined) + { + this.joined = joined; + } +} diff --git a/app/src/main/java/io/xeres/app/database/model/connection/Connection.java b/app/src/main/java/io/xeres/app/database/model/connection/Connection.java new file mode 100644 index 000000000..2c2bcbb8b --- /dev/null +++ b/app/src/main/java/io/xeres/app/database/model/connection/Connection.java @@ -0,0 +1,201 @@ +/* + * Copyright (c) 2019-2021 by David Gerber - https://zapek.com + * + * This file is part of Xeres. + * + * Xeres is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Xeres is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Xeres. If not, see . + */ + +package io.xeres.app.database.model.connection; + +import io.xeres.app.database.model.location.Location; +import io.xeres.app.net.protocol.PeerAddress; +import io.xeres.common.protocol.ip.IP; + +import javax.persistence.*; +import java.net.InetSocketAddress; +import java.net.SocketAddress; +import java.time.Instant; +import java.util.Objects; + +import static io.xeres.app.net.protocol.PeerAddress.Type.HOSTNAME; +import static io.xeres.app.net.protocol.PeerAddress.Type.IPV4; + +@Table(name = "connections") +@Entity +public class Connection +{ + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private long id; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "location_id") + private Location location; + + private PeerAddress.Type type; + + private String address; + + private Instant lastConnected; + + private boolean external; + + protected Connection() + { + } + + public static Connection from(PeerAddress peerAddress) + { + return new Connection(peerAddress); + } + + private Connection(PeerAddress peerAddress) + { + type = peerAddress.getType(); + address = peerAddress.getAddress().orElseThrow(); + external = peerAddress.isExternal(); + } + + public static Connection from(SocketAddress socketAddress) + { + if (socketAddress instanceof InetSocketAddress inetSocketAddress) + { + return new Connection(inetSocketAddress.getHostString() + ":" + inetSocketAddress.getPort()); + } + throw new IllegalArgumentException("Trying to get a connection from a non-ipv4 address"); + } + + private Connection(String address) + { + type = IPV4; + this.address = address; + external = IP.isPublicIp(address); + } + + long getId() + { + return id; + } + + void setId(long id) + { + this.id = id; + } + + public Location getLocation() + { + return location; + } + + public void setLocation(Location location) + { + this.location = location; + } + + public PeerAddress.Type getType() + { + return type; + } + + public void setType(PeerAddress.Type type) + { + this.type = type; + } + + public String getAddress() + { + return address; + } + + public void setAddress(String address) + { + this.address = address; + } + + public Instant getLastConnected() + { + return lastConnected; + } + + public void setLastConnected(Instant lastConnected) + { + this.lastConnected = lastConnected; + } + + public boolean isExternal() + { + return external; + } + + public void setExternal(boolean external) + { + this.external = external; + } + + public int getPort() + { + if (!(type == IPV4 || type == HOSTNAME)) + { + throw new IllegalArgumentException("Trying to get port from a non ipv4 address"); + } + String[] tokens = address.split(":"); + return Integer.parseInt(tokens[1]); + } + + public String getIp() + { + if (type != IPV4) + { + throw new IllegalArgumentException("Trying to get ip from a non ipv4 address"); + } + String[] tokens = address.split(":"); + return tokens[0]; + } + + public String getHostname() + { + if (type != HOSTNAME) + { + throw new IllegalArgumentException("Trying to get a hostname from a non hostname address"); + } + String[] tokens = address.split(":"); + return tokens[0]; + } + + @Override + public boolean equals(Object o) + { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + Connection that = (Connection) o; + return external == that.external && type == that.type && address.equals(that.address); + } + + @Override + public int hashCode() + { + return Objects.hash(type, address, external); + } + + @Override + public String toString() + { + return "Connection{" + + "type=" + type + + ", address='" + address + '\'' + + ", external=" + external + + '}'; + } +} diff --git a/app/src/main/java/io/xeres/app/database/model/connection/ConnectionMapper.java b/app/src/main/java/io/xeres/app/database/model/connection/ConnectionMapper.java new file mode 100644 index 000000000..3258e1bdd --- /dev/null +++ b/app/src/main/java/io/xeres/app/database/model/connection/ConnectionMapper.java @@ -0,0 +1,59 @@ +/* + * Copyright (c) 2019-2020 by David Gerber - https://zapek.com + * + * This file is part of Xeres. + * + * Xeres is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Xeres is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Xeres. If not, see . + */ + +package io.xeres.app.database.model.connection; + +import io.xeres.common.dto.connection.ConnectionDTO; + +public final class ConnectionMapper +{ + private ConnectionMapper() + { + throw new UnsupportedOperationException("Utility class"); + } + + public static ConnectionDTO toDTO(Connection connection) + { + if (connection == null) + { + return null; + } + + return new ConnectionDTO( + connection.getId(), + connection.getAddress(), + connection.getLastConnected(), + connection.isExternal()); + } + + public static Connection fromDTO(ConnectionDTO dto) + { + if (dto == null) + { + return null; + } + + var connection = new Connection(); + connection.setId(dto.id()); + connection.setAddress(dto.address()); + connection.setExternal(dto.external()); + connection.setLastConnected(dto.lastConnected()); + return connection; + } +} diff --git a/app/src/main/java/io/xeres/app/database/model/gxs/GxsCircleType.java b/app/src/main/java/io/xeres/app/database/model/gxs/GxsCircleType.java new file mode 100644 index 000000000..df715cad1 --- /dev/null +++ b/app/src/main/java/io/xeres/app/database/model/gxs/GxsCircleType.java @@ -0,0 +1,31 @@ +/* + * Copyright (c) 2019-2021 by David Gerber - https://zapek.com + * + * This file is part of Xeres. + * + * Xeres is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Xeres is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Xeres. If not, see . + */ + +package io.xeres.app.database.model.gxs; + +public enum GxsCircleType +{ + UNKNOWN, + PUBLIC, // not restricted to a circle + EXTERNAL, // restricted to an external circle + YOUR_FRIENDS_ONLY, // restricted to a subset of friend nodes + LOCAL, // not distributed at all + EXTERNAL_SELF, // self-restricted, not used except at creation time when the circle ID isn't known yet. set to external afterwards + YOUR_EYES_ONLY // distributed to locations signed by own profile only +} diff --git a/app/src/main/java/io/xeres/app/database/model/gxs/GxsClientUpdate.java b/app/src/main/java/io/xeres/app/database/model/gxs/GxsClientUpdate.java new file mode 100644 index 000000000..ff5280864 --- /dev/null +++ b/app/src/main/java/io/xeres/app/database/model/gxs/GxsClientUpdate.java @@ -0,0 +1,107 @@ +/* + * Copyright (c) 2019-2021 by David Gerber - https://zapek.com + * + * This file is part of Xeres. + * + * Xeres is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Xeres is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Xeres. If not, see . + */ + +package io.xeres.app.database.model.gxs; + +import io.xeres.app.database.model.location.Location; + +import javax.persistence.*; +import javax.validation.constraints.NotNull; +import java.time.Instant; + +@Table(name = "gxs_client_updates") +@Entity +public class GxsClientUpdate +{ + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private long id; + + @NotNull + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "location_id") + private Location location; + + @NotNull + private int serviceType; + + private Instant lastSynced; + + public GxsClientUpdate() + { + // Needed + } + + public GxsClientUpdate(Location location, int serviceType, Instant lastSynced) + { + this.location = location; + this.serviceType = serviceType; + this.lastSynced = lastSynced; + } + + public long getId() + { + return id; + } + + public void setId(long id) + { + this.id = id; + } + + public Location getLocation() + { + return location; + } + + public void setLocation(Location location) + { + this.location = location; + } + + public int getServiceType() + { + return serviceType; + } + + public void setServiceType(int serviceType) + { + this.serviceType = serviceType; + } + + public Instant getLastSynced() + { + return lastSynced; + } + + public void setLastSynced(Instant lastSynced) + { + this.lastSynced = lastSynced; + } + + @Override + public String toString() + { + return "GxsClientUpdate{" + + "location=" + location + + ", serviceType=" + serviceType + + ", lastSynced=" + lastSynced + + '}'; + } +} diff --git a/app/src/main/java/io/xeres/app/database/model/gxs/GxsGroupItem.java b/app/src/main/java/io/xeres/app/database/model/gxs/GxsGroupItem.java new file mode 100644 index 000000000..b3dd9fe42 --- /dev/null +++ b/app/src/main/java/io/xeres/app/database/model/gxs/GxsGroupItem.java @@ -0,0 +1,494 @@ +/* + * Copyright (c) 2019-2021 by David Gerber - https://zapek.com + * + * This file is part of Xeres. + * + * Xeres is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Xeres is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Xeres. If not, see . + */ + +package io.xeres.app.database.model.gxs; + +import io.netty.buffer.ByteBuf; +import io.xeres.app.database.converter.GxsPrivacyFlagsConverter; +import io.xeres.app.database.converter.GxsSignatureFlagsConverter; +import io.xeres.app.xrs.common.SecurityKey; +import io.xeres.app.xrs.common.SecurityKeySet; +import io.xeres.app.xrs.common.Signature; +import io.xeres.app.xrs.common.SignatureSet; +import io.xeres.app.xrs.item.Item; +import io.xeres.app.xrs.serialization.FieldSize; +import io.xeres.app.xrs.serialization.RsSerializable; +import io.xeres.app.xrs.serialization.SerializationFlags; +import io.xeres.app.xrs.serialization.TlvType; +import io.xeres.common.id.GxsId; +import io.xeres.common.id.LocationId; +import org.hibernate.annotations.UpdateTimestamp; + +import javax.persistence.*; +import javax.validation.constraints.NotNull; +import java.time.Instant; +import java.util.EnumSet; +import java.util.Objects; +import java.util.Set; + +import static io.xeres.app.xrs.serialization.Serializer.*; + +@Entity(name = "gxs_groups") +@Inheritance(strategy = InheritanceType.JOINED) +public abstract class GxsGroupItem extends Item implements RsSerializable +{ + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private long id; + + @Embedded + @AttributeOverride(name = "identifier", column = @Column(name = "gxs_id")) + private GxsId gxsId; + @Embedded + @AttributeOverride(name = "identifier", column = @Column(name = "original_gxs_id")) + private GxsId originalGxsId; + @NotNull + private String name; + @Convert(converter = GxsPrivacyFlagsConverter.class) + private Set diffusionFlags; + + @Convert(converter = GxsSignatureFlagsConverter.class) + private Set signatureFlags; // what signatures are required for parent and child messages + + @UpdateTimestamp + private Instant published; + + @Embedded + @AttributeOverride(name = "identifier", column = @Column(name = "author")) + private GxsId author; // author of the group, all 0 if anonymous + + @Embedded + @AttributeOverride(name = "identifier", column = @Column(name = "circle_id")) + private GxsId circleId; // id of the circle to which the group is restricted + @Enumerated + private GxsCircleType circleType = GxsCircleType.UNKNOWN; + + private int authenticationFlags; // not used yet? + @Embedded + @AttributeOverride(name = "identifier", column = @Column(name = "parent_id")) + private GxsId parentId; + + // below is local data (stored in the database only) + private int subscribeFlags; + + private int popularity; // number of friends subscribers + private int visibleMessageCount; // maximum messages reported by friends + private Instant lastPosted; // timestamp for last message + + private int status; + + // service specific storage (not synced, but they are serialized though) + private String serviceString; + @Embedded + @AttributeOverride(name = "identifier", column = @Column(name = "originator")) + private LocationId originator; + @Embedded + @AttributeOverride(name = "identifier", column = @Column(name = "internal_circle")) + private GxsId internalCircle; + + private byte[] adminPrivateKeyData; // XXX: must NOT be serialized + private byte[] adminPublicKeyData; + + private byte[] authorPrivateKeyData; // XXX: must NOT be serialized + private byte[] authorPublicKeyData; + + @Transient + private byte[] signature; + + public long getId() + { + return id; + } + + public void setId(long id) + { + this.id = id; + } + + public GxsId getGxsId() + { + return gxsId; + } + + public void setGxsId(GxsId gxsId) + { + this.gxsId = gxsId; + } + + public GxsId getOriginalGxsId() + { + return originalGxsId; + } + + public void setOriginalGxsId(GxsId originalGxsId) + { + this.originalGxsId = originalGxsId; + } + + public String getName() + { + return name; + } + + public void setName(String name) + { + this.name = name; + } + + public Set getDiffusionFlags() + { + return diffusionFlags; + } + + public void setDiffusionFlags(Set diffusionFlags) + { + this.diffusionFlags = diffusionFlags; + } + + public Set getSignatureFlags() + { + return signatureFlags; + } + + public void setSignatureFlags(Set signatureFlags) + { + this.signatureFlags = signatureFlags; + } + + public Instant getPublished() + { + return published; + } + + public void setPublished(Instant published) + { + this.published = published; + } + + public GxsId getAuthor() + { + return author; + } + + public void setAuthor(GxsId author) + { + this.author = author; + } + + public GxsId getCircleId() + { + return circleId; + } + + public void setCircleId(GxsId circleId) + { + this.circleId = circleId; + } + + public GxsCircleType getCircleType() + { + return circleType; + } + + public void setCircleType(GxsCircleType circleType) + { + this.circleType = circleType; + } + + public int getAuthenticationFlags() + { + return authenticationFlags; + } + + public void setAuthenticationFlags(int authenticationFlags) + { + this.authenticationFlags = authenticationFlags; + } + + public GxsId getParentId() + { + return parentId; + } + + public void setParentId(GxsId parentId) + { + this.parentId = parentId; + } + + public int getSubscribeFlags() + { + return subscribeFlags; + } + + public void setSubscribeFlags(int subscribeFlags) + { + this.subscribeFlags = subscribeFlags; + } + + public int getPopularity() + { + return popularity; + } + + public void setPopularity(int popularity) + { + this.popularity = popularity; + } + + public int getVisibleMessageCount() + { + return visibleMessageCount; + } + + public void setVisibleMessageCount(int visibleMessageCount) + { + this.visibleMessageCount = visibleMessageCount; + } + + public Instant getLastPosted() + { + return lastPosted; + } + + public void setLastPosted(Instant lastPosted) + { + this.lastPosted = lastPosted; + } + + public int getStatus() + { + return status; + } + + public void setStatus(int status) + { + this.status = status; + } + + public String getServiceString() + { + return serviceString; + } + + public void setServiceString(String serviceString) + { + this.serviceString = serviceString; + } + + public LocationId getOriginator() + { + return originator; + } + + public void setOriginator(LocationId originator) + { + this.originator = originator; + } + + public GxsId getInternalCircle() + { + return internalCircle; + } + + public void setInternalCircle(GxsId internalCircle) + { + this.internalCircle = internalCircle; + } + + public byte[] getAdminPrivateKeyData() + { + return adminPrivateKeyData; + } + + public void setAdminPrivateKeyData(byte[] adminPrivateKeyData) + { + this.adminPrivateKeyData = adminPrivateKeyData; + } + + public byte[] getAdminPublicKeyData() + { + return adminPublicKeyData; + } + + public void setAdminPublicKeyData(byte[] adminPublicKeyData) + { + this.adminPublicKeyData = adminPublicKeyData; + } + + public byte[] getAuthorPrivateKeyData() + { + return authorPrivateKeyData; + } + + public void setAuthorPrivateKeyData(byte[] authorPrivateKeyData) + { + this.authorPrivateKeyData = authorPrivateKeyData; + } + + public byte[] getAuthorPublicKeyData() + { + return authorPublicKeyData; + } + + public void setAuthorPublicKeyData(byte[] authorPublicKeyData) + { + this.authorPublicKeyData = authorPublicKeyData; + } + + public byte[] getSignature() + { + return signature; + } + + public void setSignature(byte[] signature) + { + this.signature = signature; + } + + @Override + public int writeObject(ByteBuf buf, Set flags) + { + int size = 0; + + // XXX: it's all in rsgxsdata.cc/serialize() and rsgenexchange.cc/createGroup()|publishGrps()|generateGroupKeys() + size += serialize(buf, 0xaf01); // current RS API (XXX: put that constant somewhere) + int sizeOffset = buf.writerIndex(); + size += serialize(buf, 0); // write size at the end + size += serialize(buf, gxsId); + size += serialize(buf, originalGxsId, GxsId.class); + size += serialize(buf, parentId, GxsId.class); + size += serialize(buf, TlvType.STRING, name); + size += serialize(buf, diffusionFlags, FieldSize.INTEGER); + size += serialize(buf, (int) published.getEpochSecond()); + size += serialize(buf, circleType); + size += serialize(buf, authenticationFlags); + size += serialize(buf, author, GxsId.class); + size += serialize(buf, TlvType.STRING, serviceString); + size += serialize(buf, circleId, GxsId.class); + size += serialize(buf, TlvType.SIGNATURE_SET, flags.contains(SerializationFlags.SIGNATURE) ? new SignatureSet() : createSignatureSet()); + size += serialize(buf, TlvType.SECURITY_KEY_SET, createSecurityKeySet()); + size += serialize(buf, signatureFlags, FieldSize.INTEGER); + buf.setInt(sizeOffset, size); // write total size + + return size; + } + + @Override + public void readObject(ByteBuf buf, Set serializationFlags) + { + int apiVersion = deserializeInt(buf); + if (apiVersion != 0xaf01) + { + throw new IllegalArgumentException("Unsupported API version " + apiVersion); + } + int size = deserializeInt(buf); // XXX: check size (this size is only the meta data size, not the full buffer) + gxsId = (GxsId) deserializeIdentifier(buf, GxsId.class); + originalGxsId = (GxsId) deserializeIdentifier(buf, GxsId.class); + parentId = (GxsId) deserializeIdentifier(buf, GxsId.class); + name = (String) deserialize(buf, TlvType.STRING); + diffusionFlags = deserializeEnumSet(buf, GxsPrivacyFlags.class, FieldSize.INTEGER); + published = Instant.ofEpochSecond(deserializeInt(buf)); + circleType = deserializeEnum(buf, GxsCircleType.class); + authenticationFlags = deserializeInt(buf); + author = (GxsId) deserializeIdentifier(buf, GxsId.class); + serviceString = (String) deserialize(buf, TlvType.STRING); + circleId = (GxsId) deserializeIdentifier(buf, GxsId.class); + deserializeSignature(buf); + deserializePublicKeys(buf); + signatureFlags = deserializeEnumSet(buf, GxsSignatureFlags.class, FieldSize.INTEGER); + } + + /** + * Creates a security key set. Note that only public keys are added to it. The private + * key has to stay private. + * + * @return a SecurityKeySet containing public keys + */ + private SecurityKeySet createSecurityKeySet() + { + var securityKeySet = new SecurityKeySet(); + if (adminPublicKeyData != null) + { + securityKeySet.put(new SecurityKey(gxsId, EnumSet.of(SecurityKey.Flags.DISTRIBUTION_ADMIN, SecurityKey.Flags.TYPE_PUBLIC_ONLY), 0, 0, adminPublicKeyData)); // XXX: ADMIN or IDENTITY (identity is when we have authorId)? + } + if (authorPublicKeyData != null) + { + securityKeySet.put(new SecurityKey(gxsId, EnumSet.of(SecurityKey.Flags.DISTRIBUTION_PUBLISH, SecurityKey.Flags.TYPE_PUBLIC_ONLY), 0, 0, authorPublicKeyData)); + } + return securityKeySet; + } + + private SignatureSet createSignatureSet() + { + Objects.requireNonNull(signature); + var signatureSet = new SignatureSet(); + signatureSet.put(SignatureSet.Type.ADMIN, new Signature(gxsId, signature)); + return signatureSet; + } + + private void deserializePublicKeys(ByteBuf buf) + { + var securityKeySet = (SecurityKeySet) deserialize(buf, TlvType.SECURITY_KEY_SET); + securityKeySet.getPublicKeys().forEach((keyId, securityKey) -> { + if (securityKey.getFlags().containsAll(Set.of(SecurityKey.Flags.DISTRIBUTION_ADMIN, SecurityKey.Flags.TYPE_PUBLIC_ONLY))) // XXX: ADMIN or IDENTITY? + { + adminPublicKeyData = securityKey.getData(); + } + else if (securityKey.getFlags().containsAll(Set.of(SecurityKey.Flags.DISTRIBUTION_PUBLISH, SecurityKey.Flags.TYPE_PUBLIC_ONLY))) + { + authorPublicKeyData = securityKey.getData(); + } + }); + } + + private void deserializeSignature(ByteBuf buf) + { + var signatureSet = (SignatureSet) deserialize(buf, TlvType.SIGNATURE_SET); + if (signatureSet.getSignatures() != null) + { + signature = signatureSet.getSignatures().get(SignatureSet.Type.ADMIN.getValue()).getData(); + } + } + + @Override + public String toString() + { + return "GxsGroupItem{" + + "id=" + id + + ", gxsId=" + gxsId + + ", name='" + name + '\'' + + ", flags=" + diffusionFlags + + ", signatureFlags=" + signatureFlags + + ", published=" + published + + ", author=" + author + + ", circleId=" + circleId + + ", circleType=" + circleType + + ", authenticationFlags=" + authenticationFlags + + ", parentId=" + parentId + + ", subscribeFlags=" + subscribeFlags + + ", popularity=" + popularity + + ", visibleMessageCount=" + visibleMessageCount + + ", lastPosted=" + lastPosted + + ", status=" + status + + ", serviceString='" + serviceString + '\'' + + ", originator=" + originator + + ", internalCircle=" + internalCircle + + '}'; + } +} diff --git a/app/src/main/java/io/xeres/app/database/model/gxs/GxsMessageItem.java b/app/src/main/java/io/xeres/app/database/model/gxs/GxsMessageItem.java new file mode 100644 index 000000000..3a4cbecda --- /dev/null +++ b/app/src/main/java/io/xeres/app/database/model/gxs/GxsMessageItem.java @@ -0,0 +1,24 @@ +/* + * Copyright (c) 2019-2021 by David Gerber - https://zapek.com + * + * This file is part of Xeres. + * + * Xeres is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Xeres is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Xeres. If not, see . + */ + +package io.xeres.app.database.model.gxs; + +public class GxsMessageItem +{ +} diff --git a/app/src/main/java/io/xeres/app/database/model/gxs/GxsPrivacyFlags.java b/app/src/main/java/io/xeres/app/database/model/gxs/GxsPrivacyFlags.java new file mode 100644 index 000000000..f7aada4fc --- /dev/null +++ b/app/src/main/java/io/xeres/app/database/model/gxs/GxsPrivacyFlags.java @@ -0,0 +1,33 @@ +/* + * Copyright (c) 2019-2021 by David Gerber - https://zapek.com + * + * This file is part of Xeres. + * + * Xeres is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Xeres is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Xeres. If not, see . + */ + +package io.xeres.app.database.model.gxs; + +public enum GxsPrivacyFlags +{ + PRIVATE, // Key needed to decrypt the publish key + RESTRICTED, // Publish private key needed to publish (eg. channels) + PUBLIC, // Anyone can publish (eg. forums) + UNUSED_4, + UNUSED_5, + UNUSED_6, + UNUSED_7, + UNUSED_8, + READ_ID +} diff --git a/app/src/main/java/io/xeres/app/database/model/gxs/GxsServiceSetting.java b/app/src/main/java/io/xeres/app/database/model/gxs/GxsServiceSetting.java new file mode 100644 index 000000000..fbf141932 --- /dev/null +++ b/app/src/main/java/io/xeres/app/database/model/gxs/GxsServiceSetting.java @@ -0,0 +1,74 @@ +/* + * Copyright (c) 2019-2021 by David Gerber - https://zapek.com + * + * This file is part of Xeres. + * + * Xeres is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Xeres is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Xeres. If not, see . + */ + +package io.xeres.app.database.model.gxs; + +import javax.persistence.Entity; +import javax.persistence.Id; +import javax.persistence.Table; +import java.time.Instant; + +@Table(name = "gxs_service_settings") +@Entity +public class GxsServiceSetting +{ + @Id + private int id; + + private Instant lastUpdated; + + public GxsServiceSetting() + { + // Needed + } + + public GxsServiceSetting(int id, Instant lastUpdated) + { + this.id = id; + this.lastUpdated = lastUpdated; + } + + public int getId() + { + return id; + } + + public void setId(int id) + { + this.id = id; + } + + public Instant getLastUpdated() + { + return lastUpdated; + } + + public void setLastUpdated(Instant lastUpdated) + { + this.lastUpdated = lastUpdated; + } + + @Override + public String toString() + { + return "GxsServiceSetting{" + + "lastUpdated=" + lastUpdated + + '}'; + } +} diff --git a/app/src/main/java/io/xeres/app/database/model/gxs/GxsSignatureFlags.java b/app/src/main/java/io/xeres/app/database/model/gxs/GxsSignatureFlags.java new file mode 100644 index 000000000..a90eb11ce --- /dev/null +++ b/app/src/main/java/io/xeres/app/database/model/gxs/GxsSignatureFlags.java @@ -0,0 +1,28 @@ +/* + * Copyright (c) 2019-2021 by David Gerber - https://zapek.com + * + * This file is part of Xeres. + * + * Xeres is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Xeres is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Xeres. If not, see . + */ + +package io.xeres.app.database.model.gxs; + +public enum GxsSignatureFlags +{ + ENCRYPTED, + ALL_SIGNED, // unused? + THREAD_HEAD, + NONE_REQUIRED +} diff --git a/app/src/main/java/io/xeres/app/database/model/identity/Identity.java b/app/src/main/java/io/xeres/app/database/model/identity/Identity.java new file mode 100644 index 000000000..a48b5590e --- /dev/null +++ b/app/src/main/java/io/xeres/app/database/model/identity/Identity.java @@ -0,0 +1,115 @@ +/* + * Copyright (c) 2019-2021 by David Gerber - https://zapek.com + * + * This file is part of Xeres. + * + * Xeres is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Xeres is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Xeres. If not, see . + */ + +package io.xeres.app.database.model.identity; + +import io.xeres.app.xrs.service.gxsid.item.GxsIdGroupItem; +import io.xeres.common.identity.Type; + +import javax.persistence.*; + +@Table(name = "identities") +@Entity +public class Identity +{ + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private long id; + + @OneToOne(fetch = FetchType.LAZY, optional = false) + @JoinColumn(name = "gxs_id") + private GxsIdGroupItem gxsIdGroupItem; + + private Type type; + + protected Identity() + { + + } + + protected Identity(GxsIdGroupItem gxsIdGroupItem, Type type) + { + this.gxsIdGroupItem = gxsIdGroupItem; + setType(type); + } + + public static Identity createOwnIdentity(GxsIdGroupItem gxsIdGroupItem, Type type) + { + if (type != Type.SIGNED && type != Type.ANONYMOUS) + { + throw new IllegalArgumentException("Wrong identity type"); + } + return new Identity(gxsIdGroupItem, type); + } + + public static Identity createFriendIdentity(GxsIdGroupItem gxsIdGroupItem) + { + return new Identity(gxsIdGroupItem, Type.FRIEND); + } + + public static Identity createIdentity(GxsIdGroupItem gxsIdGroupItem) + { + return new Identity(gxsIdGroupItem, Type.OTHER); + } + + public long getId() + { + return id; + } + + public void setId(long id) + { + this.id = id; + } + + public Type getType() + { + return type; + } + + public void setType(Type type) + { + this.type = type; + } + + public GxsIdGroupItem getGxsIdGroupItem() + { + return gxsIdGroupItem; + } + + /** + * Checks if an identity must be kept in the list of own identities and friends. + * + * @return true if important enough + */ + public boolean isNotable() + { + return type != Type.OTHER; + } + + @Override + public String toString() + { + return "Identity{" + + "id=" + id + + ", gxsIdGroupItem=" + gxsIdGroupItem + + ", type=" + type + + '}'; + } +} diff --git a/app/src/main/java/io/xeres/app/database/model/location/Location.java b/app/src/main/java/io/xeres/app/database/model/location/Location.java new file mode 100644 index 000000000..c3be3e8c8 --- /dev/null +++ b/app/src/main/java/io/xeres/app/database/model/location/Location.java @@ -0,0 +1,352 @@ +/* + * Copyright (c) 2019-2021 by David Gerber - https://zapek.com + * + * This file is part of Xeres. + * + * Xeres is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Xeres is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Xeres. If not, see . + */ + +package io.xeres.app.database.model.location; + +import io.xeres.app.crypto.rsid.RSId; +import io.xeres.app.crypto.rsid.certificate.RSCertificate; +import io.xeres.app.crypto.rsid.shortinvite.ShortInvite; +import io.xeres.app.database.model.connection.Connection; +import io.xeres.app.database.model.profile.Profile; +import io.xeres.common.id.LocationId; +import io.xeres.common.protocol.NetMode; + +import javax.persistence.*; +import javax.validation.constraints.NotNull; +import java.security.cert.CertificateParsingException; +import java.time.Instant; +import java.util.ArrayList; +import java.util.List; +import java.util.Objects; +import java.util.stream.Stream; + +import static io.xeres.common.dto.location.LocationConstants.OWN_LOCATION_ID; +import static java.util.Comparator.*; + +@Table(name = "locations") +@Entity +public class Location +{ + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private long id; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "profile_id") + private Profile profile; + + @NotNull + private String name; + + @Embedded + @NotNull + @AttributeOverride(name = "identifier", column = @Column(name = "location_identifier")) + private LocationId locationId; + + @OneToMany(fetch = FetchType.LAZY, cascade = CascadeType.ALL, mappedBy = "location", orphanRemoval = true) + private final List connections = new ArrayList<>(); + + private boolean connected; + + private Instant lastConnected; + + private boolean discoverable = true; + + private boolean dht = true; + + @Transient + private String version; + + private NetMode netMode = NetMode.UNKNOWN; + + protected Location() + { + + } + + protected Location(String name) + { + this.name = name; + } + + protected Location(long id, String name, Profile profile, LocationId locationId) + { + this.id = id; + this.name = name; + this.profile = profile; + this.locationId = locationId; + } + + protected Location(String name, Profile profile, LocationId locationId) + { + this.name = name; + this.profile = profile; + this.locationId = locationId; + } + + public static Location createLocation(RSId rsId) + { + return new Location(rsId); + } + + public static Location createLocation(String name) + { + return new Location(name); + } + + public static Location createLocation(String name, Profile profile, LocationId locationId) + { + return new Location(name, profile, locationId); + } + + public static void addOrUpdateLocations(Profile profile, Location newLocation) + { + profile.getLocations().removeIf(oldLocation -> oldLocation.getLocationId().equals(newLocation.getLocationId())); // XXX: don't remove but update if there are additional fields that were gathered before an update (ie. additional IPs) + profile.addLocation(newLocation); + } + + public Location(RSId rsId) + { + setName(rsId.getName()); // XXX: how do we handle the constraints? + setLocationId(rsId.getLocationId()); + // XXX: add connections from: hostname, internal, external, locators (ipv4 and ipv6), hidden + // XXX: also we should have validation of internal IPs (192.168, etc... 169, etc...) and external IP to avoid the current Retroshare mess that attempts connecting to bullshit IPs + // XXX: RsUrls *DO* have a port! eg, I have ipv4://169.254.209.149:11416, 169.254.67.38:11416, 172.18.23.225:11416, 169.254.167.200:11416, 10.0.75.1:11416, 172.17.153.241:11416, etc... only one with external port :) + if (rsId.hasDnsName()) // XXX: this will not work with certificates! + { + addConnection(Connection.from(rsId.getDnsName())); + } + if (rsId.hasInternalIp()) + { + addConnection(Connection.from(rsId.getInternalIp())); + } + if (rsId.hasExternalIp()) + { + addConnection(Connection.from(rsId.getExternalIp())); // XXX: this one has the proper port to use for external connections. how to handle it? what did I mean by that comment? + } + + // XXX: continue here + // XXX: the internal/external ipv4/port thing should use the same mechanism as RsUrl I believe + } + + public RSId getRSId() + { + return getShortInvite(); + // XXX: here always return a shortId since this is the new format (can this be adjusted?) + //return RSCertificate.createRSCertificateFromLocation(location); + } + + public ShortInvite getShortInvite() + { + var si = new ShortInvite(); + + si.setName(getProfile().getName().getBytes()); + si.setLocationId(getLocationId()); + si.setPgpFingerprint(getProfile().getProfileFingerprint().getBytes()); + getConnections().forEach(connection -> + { + if (connection.isExternal()) + { + si.setExt4Locator(connection.getAddress()); + } + else + { + si.setLoc4Locator(connection.getAddress()); + } + }); + return si; + } + + public RSCertificate getRSCertificate() throws CertificateParsingException + { + var rsc = new RSCertificate(); + + rsc.setName(getName()); + rsc.setLocationId(getLocationId()); + rsc.setPgpPublicKey(getProfile().getPgpPublicKeyData()); + getConnections().forEach(connection -> { + if (connection.isExternal()) + { + rsc.setExternalIp(connection.getAddress()); + } + else + { + rsc.setInternalIp(connection.getAddress()); + } + }); + return rsc; + } + + public void addConnection(Connection connection) + { + connection.setLocation(this); + getConnections().add(connection); + } + + public Profile getProfile() + { + return profile; + } + + public void setProfile(Profile profile) + { + this.profile = profile; + } + + public long getId() + { + return id; + } + + void setId(long id) + { + this.id = id; + } + + public void setName(String name) + { + this.name = name; + } + + public String getName() + { + return name; + } + + public boolean isConnected() + { + return connected; + } + + public void setConnected(boolean connected) + { + this.connected = connected; + setLastConnected(Instant.now()); + } + + public boolean isDiscoverable() + { + return discoverable; + } + + public void setDiscoverable(boolean discoverable) + { + this.discoverable = discoverable; + } + + public boolean isDht() + { + return dht; + } + + public void setDht(boolean dht) + { + this.dht = dht; + } + + public String getVersion() + { + return version; + } + + public void setVersion(String version) + { + this.version = version; + } + + public NetMode getNetMode() + { + return netMode; + } + + public void setNetMode(NetMode netMode) + { + this.netMode = netMode; + } + + public void setLocationId(LocationId locationId) + { + this.locationId = locationId; + } + + public LocationId getLocationId() + { + return locationId; + } + + public List getConnections() + { + return connections; + } + + public Instant getLastConnected() + { + return lastConnected; + } + + public void setLastConnected(Instant lastConnected) + { + this.lastConnected = lastConnected; + } + + public boolean isOwn() + { + return id == OWN_LOCATION_ID; + } + + /** + * Returns the best connection. Prefers connections most recently connected to. + * + * @param index index of the connection, is supposed to always increment so that a different connection is returned + * @return a connection or empty if none + */ + public Stream getBestConnection(int index) + { + if (connections.isEmpty()) + { + return Stream.empty(); + } + List connectionsSortedByMostReliable = connections.stream() + .sorted(comparing(Connection::getLastConnected, nullsLast(naturalOrder()))) + .toList(); + + return Stream.of(connectionsSortedByMostReliable.get(index % connectionsSortedByMostReliable.size())); + } + + @Override + public boolean equals(Object o) + { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + var location = (Location) o; + return locationId.equals(location.locationId); + } + + @Override + public int hashCode() + { + return Objects.hash(locationId); + } + + @Override + public String toString() + { + return locationId.toString(); + } +} diff --git a/app/src/main/java/io/xeres/app/database/model/location/LocationMapper.java b/app/src/main/java/io/xeres/app/database/model/location/LocationMapper.java new file mode 100644 index 000000000..b8d19e8c4 --- /dev/null +++ b/app/src/main/java/io/xeres/app/database/model/location/LocationMapper.java @@ -0,0 +1,83 @@ +/* + * Copyright (c) 2019-2020 by David Gerber - https://zapek.com + * + * This file is part of Xeres. + * + * Xeres is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Xeres is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Xeres. If not, see . + */ + +package io.xeres.app.database.model.location; + +import io.xeres.app.database.model.connection.ConnectionMapper; +import io.xeres.common.dto.location.LocationDTO; +import io.xeres.common.id.LocationId; + +import java.util.ArrayList; + +public final class LocationMapper +{ + private LocationMapper() + { + throw new UnsupportedOperationException("Utility class"); + } + + public static LocationDTO toDTO(Location location) + { + if (location == null) + { + return null; + } + + return new LocationDTO( + location.getId(), + location.getName(), + location.getLocationId().getBytes(), + null, + new ArrayList<>(), + location.isConnected(), + location.getLastConnected() + ); + } + + public static LocationDTO toDeepDTO(Location location) + { + if (location == null) + { + return null; + } + var locationDTO = toDTO(location); + + locationDTO.connections().addAll(location.getConnections().stream() + .map(ConnectionMapper::toDTO) + .toList()); + + return locationDTO; + } + + public static Location fromDTO(LocationDTO dto) + { + if (dto == null) + { + return null; + } + + var location = new Location(); + location.setId(dto.id()); + location.setName(dto.name()); + location.setLocationId(new LocationId(dto.locationIdentifier())); + location.setConnected(dto.connected()); + location.setLastConnected(dto.lastConnected()); + return location; + } +} diff --git a/app/src/main/java/io/xeres/app/database/model/prefs/Prefs.java b/app/src/main/java/io/xeres/app/database/model/prefs/Prefs.java new file mode 100644 index 000000000..ee33cb6f4 --- /dev/null +++ b/app/src/main/java/io/xeres/app/database/model/prefs/Prefs.java @@ -0,0 +1,88 @@ +/* + * Copyright (c) 2019-2020 by David Gerber - https://zapek.com + * + * This file is part of Xeres. + * + * Xeres is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Xeres is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Xeres. If not, see . + */ + +package io.xeres.app.database.model.prefs; + +import javax.persistence.Entity; +import javax.persistence.Id; +import javax.persistence.Table; + +@Table(name = "prefs") +@Entity +public class Prefs +{ + @SuppressWarnings("unused") + @Id + private final byte lock = 1; + + private byte[] pgpPrivateKeyData; + + private byte[] locationPrivateKeyData; + private byte[] locationPublicKeyData; + private byte[] locationCertificate; + + protected Prefs() + { + } + + public byte[] getPgpPrivateKeyData() + { + return pgpPrivateKeyData; + } + + public void setPgpPrivateKeyData(byte[] keyData) + { + this.pgpPrivateKeyData = keyData; + } + + public byte[] getLocationPrivateKeyData() + { + return locationPrivateKeyData; + } + + public void setLocationPrivateKeyData(byte[] keyData) + { + this.locationPrivateKeyData = keyData; + } + + public byte[] getLocationPublicKeyData() + { + return locationPublicKeyData; + } + + public void setLocationPublicKeyData(byte[] keyData) + { + this.locationPublicKeyData = keyData; + } + + public byte[] getLocationCertificate() + { + return locationCertificate; + } + + public void setLocationCertificate(byte[] certificate) + { + this.locationCertificate = certificate; + } + + public boolean hasLocationCertificate() + { + return locationCertificate != null; + } +} diff --git a/app/src/main/java/io/xeres/app/database/model/profile/Profile.java b/app/src/main/java/io/xeres/app/database/model/profile/Profile.java new file mode 100644 index 000000000..61aeaae78 --- /dev/null +++ b/app/src/main/java/io/xeres/app/database/model/profile/Profile.java @@ -0,0 +1,241 @@ +/* + * Copyright (c) 2019-2021 by David Gerber - https://zapek.com + * + * This file is part of Xeres. + * + * Xeres is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Xeres is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Xeres. If not, see . + */ + +package io.xeres.app.database.model.profile; + +import io.xeres.app.database.model.location.Location; +import io.xeres.common.id.ProfileFingerprint; +import io.xeres.common.pgp.Trust; +import org.bouncycastle.util.encoders.Hex; + +import javax.persistence.*; +import javax.validation.constraints.NotNull; +import javax.validation.constraints.Size; +import java.util.ArrayList; +import java.util.List; + +import static io.xeres.common.dto.profile.ProfileConstants.*; + +@Table(name = "profiles") +@Entity +public class Profile +{ + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private long id; + + @NotNull + @Size(min = NAME_LENGTH_MIN, max = NAME_LENGTH_MAX) + private String name; + + @NotNull + private long pgpIdentifier; + + @Embedded + @NotNull + @AttributeOverride(name = "identifier", column = @Column(name = "pgp_fingerprint")) + private ProfileFingerprint profileFingerprint; + + private byte[] pgpPublicKeyData; // if null, this is not a valid profile yet + + private boolean accepted; + + private Trust trust = Trust.UNKNOWN; + + @OneToMany(fetch = FetchType.LAZY, cascade = CascadeType.ALL, mappedBy = "profile", orphanRemoval = true) + private final List locations = new ArrayList<>(); + + protected Profile() + { + } + + // This is only used for unit tests + protected Profile(long id, String name, long pgpIdentifier, ProfileFingerprint profileFingerprint, byte[] pgpPublicKeyData) + { + this(name, pgpIdentifier, profileFingerprint, pgpPublicKeyData); + this.id = id; + } + + public static Profile createOwnProfile(String name, long pgpIdentifier, ProfileFingerprint profileFingerprint, byte[] pgpPublicKeyData) + { + var profile = new Profile(name, pgpIdentifier, profileFingerprint, pgpPublicKeyData); + profile.setTrust(Trust.ULTIMATE); + profile.setAccepted(true); + return profile; + } + + public static Profile createProfile(String name, long pgpIdentifier, byte[] pgpFingerprint, byte[] pgpPublicKeyData) + { + return new Profile(name, pgpIdentifier, new ProfileFingerprint(pgpFingerprint), pgpPublicKeyData); + } + + public static Profile createProfile(String name, long pgpIdentifier, ProfileFingerprint profileFingerprint, byte[] pgpPublicKeyData) + { + return new Profile(name, pgpIdentifier, profileFingerprint, pgpPublicKeyData); + } + + public static Profile createEmptyProfile(String name, long pgpIdentifier, ProfileFingerprint profileFingerprint) + { + return new Profile(name, pgpIdentifier, profileFingerprint, null); + } + + private Profile(String name, long pgpIdentifier, ProfileFingerprint profileFingerprint, byte[] pgpPublicKeyData) + { + this.name = sanitizeProfileName(name); + this.pgpIdentifier = pgpIdentifier; + this.profileFingerprint = profileFingerprint; + this.pgpPublicKeyData = pgpPublicKeyData; + } + + private static String sanitizeProfileName(String profileName) + { + int index = profileName.indexOf(" (Generated by"); + if (index > 0) + { + return profileName.substring(0, index); + } + return profileName; + } + + public Profile updateWith(Profile other) + { + if (isPartial() && other.isComplete()) + { + setPgpPublicKeyData(other.getPgpPublicKeyData()); // Promote to full profile + } + Location.addOrUpdateLocations(this, other.getLocations().stream().findFirst().orElseThrow(() -> new IllegalStateException("Missing location"))); + return this; + } + + public void addLocation(Location location) + { + location.setProfile(this); + getLocations().add(location); + } + + public long getId() + { + return id; + } + + void setId(long id) + { + this.id = id; + } + + public String getName() + { + return name; + } + + void setName(String name) + { + this.name = name; + } + + public long getPgpIdentifier() + { + return pgpIdentifier; + } + + void setPgpIdentifier(long pgpIdentifier) + { + this.pgpIdentifier = pgpIdentifier; + } + + public ProfileFingerprint getProfileFingerprint() + { + return profileFingerprint; + } + + public void setProfileFingerprint(ProfileFingerprint profileFingerprint) + { + this.profileFingerprint = profileFingerprint; + } + + public byte[] getPgpPublicKeyData() + { + return pgpPublicKeyData; + } + + public void setPgpPublicKeyData(byte[] pgpPublicKeyData) + { + this.pgpPublicKeyData = pgpPublicKeyData; + } + + public boolean isAccepted() + { + return accepted; + } + + public void setAccepted(boolean accepted) + { + this.accepted = accepted; + } + + public Trust getTrust() + { + return trust; + } + + void setTrust(Trust trust) + { + this.trust = trust; + } + + public List getLocations() + { + return locations; + } + + public static boolean isOwn(long id) + { + return id == OWN_PROFILE_ID; + } + + public boolean isOwn() + { + return id == OWN_PROFILE_ID; + } + + public boolean isComplete() + { + return pgpPublicKeyData != null; + } + + public boolean isPartial() + { + return pgpPublicKeyData == null; + } + + @Override + public String toString() + { + return "Profile{" + + "id=" + id + + ", name='" + name + '\'' + + ", pgpIdentifier=" + io.xeres.common.id.Id.toString(pgpIdentifier) + + ", profileFingerprint=" + profileFingerprint + + ", pgpPublicKeyData=" + new String(Hex.encode(pgpPublicKeyData)) + + ", accepted=" + accepted + + ", trust=" + trust + + ", locations=" + locations + + '}'; + } +} diff --git a/app/src/main/java/io/xeres/app/database/model/profile/ProfileMapper.java b/app/src/main/java/io/xeres/app/database/model/profile/ProfileMapper.java new file mode 100644 index 000000000..0366d998a --- /dev/null +++ b/app/src/main/java/io/xeres/app/database/model/profile/ProfileMapper.java @@ -0,0 +1,112 @@ +/* + * Copyright (c) 2019-2021 by David Gerber - https://zapek.com + * + * This file is part of Xeres. + * + * Xeres is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Xeres is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Xeres. If not, see . + */ + +package io.xeres.app.database.model.profile; + +import io.xeres.app.database.model.location.LocationMapper; +import io.xeres.common.dto.profile.ProfileDTO; +import io.xeres.common.id.ProfileFingerprint; + +import java.util.ArrayList; +import java.util.List; + +import static java.util.Collections.emptyList; + +public final class ProfileMapper +{ + private ProfileMapper() + { + throw new UnsupportedOperationException("Utility class"); + } + + public static ProfileDTO toDTO(Profile profile) + { + if (profile == null) + { + return null; + } + + return new ProfileDTO( + profile.getId(), + profile.getName(), + Long.toString(profile.getPgpIdentifier()), + profile.getProfileFingerprint().getBytes(), + profile.getPgpPublicKeyData(), + profile.isAccepted(), + profile.getTrust(), + new ArrayList<>()); + } + + public static ProfileDTO toDeepDTO(Profile profile) + { + if (profile == null) + { + return null; + } + var profileDTO = toDTO(profile); + + profileDTO.locations().addAll(profile.getLocations().stream() + .map(LocationMapper::toDeepDTO) + .toList()); + + return profileDTO; + } + + public static List toDTOs(List profiles) + { + if (profiles == null) + { + return emptyList(); + } + + return profiles.stream() + .map(ProfileMapper::toDTO) + .toList(); + } + + public static List toDeepDTOs(List profiles) + { + if (profiles == null) + { + return emptyList(); + } + + return profiles.stream() + .map(ProfileMapper::toDeepDTO) + .toList(); + } + + public static Profile fromDTO(ProfileDTO dto) + { + if (dto == null) + { + return null; + } + + var profile = new Profile(); + profile.setId(dto.id()); + profile.setName(dto.name()); + profile.setPgpIdentifier(Long.parseLong(dto.pgpIdentifier())); + profile.setProfileFingerprint(new ProfileFingerprint(dto.pgpFingerprint())); + profile.setPgpPublicKeyData(dto.pgpPublicKeyData()); + profile.setAccepted(dto.accepted()); + profile.setTrust(dto.trust()); + return profile; + } +} diff --git a/app/src/main/java/io/xeres/app/database/repository/ChatRoomRepository.java b/app/src/main/java/io/xeres/app/database/repository/ChatRoomRepository.java new file mode 100644 index 000000000..624ccc14c --- /dev/null +++ b/app/src/main/java/io/xeres/app/database/repository/ChatRoomRepository.java @@ -0,0 +1,42 @@ +/* + * Copyright (c) 2019-2021 by David Gerber - https://zapek.com + * + * This file is part of Xeres. + * + * Xeres is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Xeres is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Xeres. If not, see . + */ + +package io.xeres.app.database.repository; + +import io.xeres.app.database.model.chatroom.ChatRoom; +import io.xeres.app.database.model.identity.Identity; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Modifying; +import org.springframework.data.jpa.repository.Query; +import org.springframework.stereotype.Repository; + +import java.util.List; +import java.util.Optional; + +@Repository +public interface ChatRoomRepository extends JpaRepository +{ + Optional findByRoomIdAndIdentity(long roomId, Identity identity); + + List findAllBySubscribedTrueAndJoinedFalse(); + + @Modifying + @Query("UPDATE ChatRoom c SET c.joined = false WHERE c.joined = true") + void putAllJoinedToFalse(); +} diff --git a/app/src/main/java/io/xeres/app/database/repository/GxsClientUpdateRepository.java b/app/src/main/java/io/xeres/app/database/repository/GxsClientUpdateRepository.java new file mode 100644 index 000000000..06df0b53c --- /dev/null +++ b/app/src/main/java/io/xeres/app/database/repository/GxsClientUpdateRepository.java @@ -0,0 +1,33 @@ +/* + * Copyright (c) 2019-2021 by David Gerber - https://zapek.com + * + * This file is part of Xeres. + * + * Xeres is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Xeres is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Xeres. If not, see . + */ + +package io.xeres.app.database.repository; + +import io.xeres.app.database.model.gxs.GxsClientUpdate; +import io.xeres.app.database.model.location.Location; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; + +import java.util.Optional; + +@Repository +public interface GxsClientUpdateRepository extends JpaRepository +{ + Optional findByLocationAndServiceType(Location location, int serviceType); +} diff --git a/app/src/main/java/io/xeres/app/database/repository/GxsIdRepository.java b/app/src/main/java/io/xeres/app/database/repository/GxsIdRepository.java new file mode 100644 index 000000000..cd54e25af --- /dev/null +++ b/app/src/main/java/io/xeres/app/database/repository/GxsIdRepository.java @@ -0,0 +1,33 @@ +/* + * Copyright (c) 2019-2021 by David Gerber - https://zapek.com + * + * This file is part of Xeres. + * + * Xeres is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Xeres is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Xeres. If not, see . + */ + +package io.xeres.app.database.repository; + +import io.xeres.app.xrs.service.gxsid.item.GxsIdGroupItem; +import io.xeres.common.id.GxsId; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; + +import java.util.Optional; + +@Repository +public interface GxsIdRepository extends JpaRepository +{ + Optional findByGxsId(GxsId gxsId); +} diff --git a/app/src/main/java/io/xeres/app/database/repository/GxsServiceSettingRepository.java b/app/src/main/java/io/xeres/app/database/repository/GxsServiceSettingRepository.java new file mode 100644 index 000000000..bbcf98ce9 --- /dev/null +++ b/app/src/main/java/io/xeres/app/database/repository/GxsServiceSettingRepository.java @@ -0,0 +1,29 @@ +/* + * Copyright (c) 2019-2021 by David Gerber - https://zapek.com + * + * This file is part of Xeres. + * + * Xeres is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Xeres is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Xeres. If not, see . + */ + +package io.xeres.app.database.repository; + +import io.xeres.app.database.model.gxs.GxsServiceSetting; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; + +@Repository +public interface GxsServiceSettingRepository extends JpaRepository +{ +} diff --git a/app/src/main/java/io/xeres/app/database/repository/IdentityRepository.java b/app/src/main/java/io/xeres/app/database/repository/IdentityRepository.java new file mode 100644 index 000000000..d08722be9 --- /dev/null +++ b/app/src/main/java/io/xeres/app/database/repository/IdentityRepository.java @@ -0,0 +1,30 @@ +/* + * Copyright (c) 2019-2021 by David Gerber - https://zapek.com + * + * This file is part of Xeres. + * + * Xeres is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Xeres is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Xeres. If not, see . + */ + +package io.xeres.app.database.repository; + +import io.xeres.app.database.model.identity.Identity; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; + +@Repository +public interface IdentityRepository extends JpaRepository +{ + +} diff --git a/app/src/main/java/io/xeres/app/database/repository/LocationRepository.java b/app/src/main/java/io/xeres/app/database/repository/LocationRepository.java new file mode 100644 index 000000000..9b18507b1 --- /dev/null +++ b/app/src/main/java/io/xeres/app/database/repository/LocationRepository.java @@ -0,0 +1,46 @@ +/* + * Copyright (c) 2019-2020 by David Gerber - https://zapek.com + * + * This file is part of Xeres. + * + * Xeres is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Xeres is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Xeres. If not, see . + */ + +package io.xeres.app.database.repository; + +import io.xeres.app.database.model.location.Location; +import io.xeres.common.id.LocationId; +import org.springframework.data.domain.Pageable; +import org.springframework.data.domain.Slice; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Modifying; +import org.springframework.data.jpa.repository.Query; +import org.springframework.stereotype.Repository; + +import java.util.List; +import java.util.Optional; + +@Repository +public interface LocationRepository extends JpaRepository +{ + Optional findByLocationId(LocationId locationId); + + Slice findAllByConnectedFalse(Pageable pageable); + + List findAllByConnectedTrue(); + + @Modifying + @Query("UPDATE Location l SET l.connected = false WHERE l.connected = true") + void putAllConnectedToFalse(); +} diff --git a/app/src/main/java/io/xeres/app/database/repository/PrefsRepository.java b/app/src/main/java/io/xeres/app/database/repository/PrefsRepository.java new file mode 100644 index 000000000..3d9880f12 --- /dev/null +++ b/app/src/main/java/io/xeres/app/database/repository/PrefsRepository.java @@ -0,0 +1,35 @@ +/* + * Copyright (c) 2019-2021 by David Gerber - https://zapek.com + * + * This file is part of Xeres. + * + * Xeres is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Xeres is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Xeres. If not, see . + */ + +package io.xeres.app.database.repository; + +import io.xeres.app.database.model.prefs.Prefs; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Modifying; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; +import org.springframework.stereotype.Repository; + +@Repository +public interface PrefsRepository extends JpaRepository +{ + @Modifying + @Query(value = "BACKUP TO :file", nativeQuery = true) + void backupDatabase(@Param("file") String file); +} diff --git a/app/src/main/java/io/xeres/app/database/repository/ProfileRepository.java b/app/src/main/java/io/xeres/app/database/repository/ProfileRepository.java new file mode 100644 index 000000000..3297042a7 --- /dev/null +++ b/app/src/main/java/io/xeres/app/database/repository/ProfileRepository.java @@ -0,0 +1,50 @@ +/* + * Copyright (c) 2019-2020 by David Gerber - https://zapek.com + * + * This file is part of Xeres. + * + * Xeres is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Xeres is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Xeres. If not, see . + */ + +package io.xeres.app.database.repository; + +import io.xeres.app.database.model.profile.Profile; +import io.xeres.common.id.LocationId; +import io.xeres.common.id.ProfileFingerprint; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; +import org.springframework.stereotype.Repository; + +import java.util.List; +import java.util.Optional; + +@Repository +public interface ProfileRepository extends JpaRepository +{ + Optional findByName(String name); + + Optional findByProfileFingerprint(ProfileFingerprint profileFingerprint); + + Optional findByPgpIdentifier(long pgpIdentifier); + + @Query("SELECT p FROM Profile p, IN(p.locations) l WHERE l.locationId = :locationId") + Optional findProfileByLocationId(@Param("locationId") LocationId locationId); + + @Query("SELECT p FROM Profile p, IN(p.locations) l WHERE p.pgpIdentifier = :pgpIdentifier AND p.accepted = true AND p.pgpPublicKeyData is not null AND l.discoverable = true") + Optional findDiscoverableProfileByPgpIdentifier(@Param("pgpIdentifier") long pgpIdentifier); + + @Query("SELECT p FROM Profile p, IN(p.locations) l WHERE p.accepted = true AND p.pgpPublicKeyData is not null AND l.discoverable = true") + List getAllDiscoverableProfiles(); +} diff --git a/app/src/main/java/io/xeres/app/job/PeerConnectionJob.java b/app/src/main/java/io/xeres/app/job/PeerConnectionJob.java new file mode 100644 index 000000000..bc78bd216 --- /dev/null +++ b/app/src/main/java/io/xeres/app/job/PeerConnectionJob.java @@ -0,0 +1,78 @@ +/* + * Copyright (c) 2019-2020 by David Gerber - https://zapek.com + * + * This file is part of Xeres. + * + * Xeres is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Xeres is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Xeres. If not, see . + */ + +package io.xeres.app.job; + +import io.xeres.app.XeresApplication; +import io.xeres.app.database.model.connection.Connection; +import io.xeres.app.net.peer.bootstrap.PeerClient; +import io.xeres.app.net.protocol.PeerAddress; +import io.xeres.app.service.LocationService; +import io.xeres.common.properties.StartupProperties; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.scheduling.annotation.Scheduled; +import org.springframework.stereotype.Component; + +import java.util.List; + +@Component +public class PeerConnectionJob +{ + private static final Logger log = LoggerFactory.getLogger(PeerConnectionJob.class); + + private final LocationService locationService; + private final PeerClient peerClient; + + public PeerConnectionJob(LocationService locationService, PeerClient peerClient) + { + this.locationService = locationService; + this.peerClient = peerClient; + } + + @Scheduled(initialDelay = 5 * 1000, fixedDelay = 60 * 1000) + protected void checkConnections() + { + // XXX: don't try to connect when we're shutting down... I wonder if this is a spring boot bug... maybe ask them? + if (!StartupProperties.getBoolean(StartupProperties.Property.SERVER_ONLY, false) && !XeresApplication.isRemoteUiClient()) + { + connectToPeers(); + } + } + + private void connectToPeers() + { + // XXX: check if the network is UP before attempting + List connections = locationService.getConnectionsToConnectTo(); + + for (Connection connection : connections) + { + log.debug("Attempting to connect to {} ...", connection.getAddress()); + var peerAddress = PeerAddress.fromAddress(connection.getAddress()); + if (peerAddress.isValid()) + { + peerClient.connect(peerAddress); + } + else + { + log.error("Automatic connection: invalid address for {}", connection.getAddress()); + } + } + } +} diff --git a/app/src/main/java/io/xeres/app/net/bdisc/BroadcastDiscoveryService.java b/app/src/main/java/io/xeres/app/net/bdisc/BroadcastDiscoveryService.java new file mode 100644 index 000000000..263fcedfe --- /dev/null +++ b/app/src/main/java/io/xeres/app/net/bdisc/BroadcastDiscoveryService.java @@ -0,0 +1,356 @@ +/* + * Copyright (c) 2019-2021 by David Gerber - https://zapek.com + * + * This file is part of Xeres. + * + * Xeres is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Xeres is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Xeres. If not, see . + */ + +package io.xeres.app.net.bdisc; + +import io.xeres.app.database.DatabaseSession; +import io.xeres.app.database.DatabaseSessionManager; +import io.xeres.app.database.model.connection.Connection; +import io.xeres.app.net.protocol.PeerAddress; +import io.xeres.app.service.LocationService; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.stereotype.Service; + +import java.io.IOException; +import java.net.*; +import java.nio.ByteBuffer; +import java.nio.channels.ClosedByInterruptException; +import java.nio.channels.DatagramChannel; +import java.nio.channels.SelectionKey; +import java.nio.channels.Selector; +import java.time.Duration; +import java.time.Instant; +import java.util.*; +import java.util.concurrent.ThreadLocalRandom; + +/** + * This service periodically sends an UDP broadcast packet to find out + * if other Retroshare clients are on the LAN. It implements more or + * less the same protocol as found in https://github.com/truvorskameikin/udp-discovery-cpp + * (which is what Retroshare uses). + */ +@Service +public class BroadcastDiscoveryService implements Runnable +{ + private static final Logger log = LoggerFactory.getLogger(BroadcastDiscoveryService.class); + + private static final int PORT = 36405; + private static final int APP_ID = 904571; + private static final int BROADCAST_BUFFER_SEND_SIZE_MAX = 512; + private static final int BROADCAST_BUFFER_RECV_SIZE = 512; + + private static final Duration BROADCAST_MAX_WAIT_TIME = Duration.ofSeconds(5); + + private enum State + { + BROADCASTING, + WAITING, + INTERRUPTED + } + + private final DatabaseSessionManager databaseSessionManager; + private final LocationService locationService; + + private InetSocketAddress localAddress; + private InetSocketAddress sendAddress; + private Thread thread; + + private SocketAddress broadcastAddress; + private ByteBuffer sendBuffer; + private ByteBuffer receiveBuffer; + private State state; + private Instant sent = Instant.EPOCH; + private int ownPeerId; + private final int counter = 1; + private final Map peers = new HashMap<>(); + + public BroadcastDiscoveryService(DatabaseSessionManager databaseSessionManager, LocationService locationService) + { + this.databaseSessionManager = databaseSessionManager; + this.locationService = locationService; + } + + public void start(String localIpAddress, int localPort) + { + log.info("Starting Broadcast Discovery..."); + this.localAddress = new InetSocketAddress(localIpAddress, localPort); + thread = new Thread(this, "Broadcast Discovery Service"); + thread.start(); + } + + public void stop() + { + if (thread != null) + { + log.info("Stopping Broadcast Discovery..."); + thread.interrupt(); + } + } + + public boolean isRunning() + { + return thread.isAlive(); + } + + public void waitForTermination() + { + if (thread != null) + { + try + { + log.info("Waiting for Broadcast Discovery to terminate..."); + thread.join(); + } + catch (InterruptedException e) + { + log.error("Failed to wait for termination: {}", e.getMessage(), e); + Thread.currentThread().interrupt(); + } + } + } + + private String getBroadcastAddress(String ipAddress) + { + List broadcastList = new ArrayList<>(); + + Iterator interfaces; + try + { + interfaces = NetworkInterface.getNetworkInterfaces().asIterator(); + while (interfaces.hasNext()) + { + var networkInterface = interfaces.next(); + if (networkInterface.isUp() && !networkInterface.isLoopback()) + { + networkInterface.getInterfaceAddresses().stream() + .filter(interfaceAddress -> interfaceAddress.getAddress().getHostAddress().equals(ipAddress)) + .map(InterfaceAddress::getBroadcast) + .filter(Objects::nonNull) + .forEach(broadcastList::add); + } + } + } + catch (SocketException e) + { + throw new IllegalStateException("Couldn't find broadcast address: " + e.getMessage(), e); + } + return broadcastList.stream().findFirst().orElseThrow(() -> new IllegalStateException("No broadcast address found")).getHostAddress(); + } + + private void setupOwnInfo() + { + try (var session = new DatabaseSession(databaseSessionManager)) + { + var ownLocation = locationService.findOwnLocation().orElseThrow(); + + ownPeerId = ThreadLocalRandom.current().nextInt(); + sendBuffer = UdpDiscoveryProtocol.createPacket( + BROADCAST_BUFFER_SEND_SIZE_MAX, + UdpDiscoveryPeer.Status.PRESENT, + APP_ID, + ownPeerId, + counter, + ownLocation.getProfile().getProfileFingerprint(), + ownLocation.getLocationId(), + localAddress.getPort(), + ownLocation.getProfile().getName()); + sendBuffer.flip(); + } + } + + private void updateOwnInfo() + { + // For now we do nothing but we could implement something better if for + // example there's a change of IP or port. Don't forget to increase the + // counter for each update otherwise it won't be taken into account. + } + + @Override + public void run() + { + broadcastAddress = new InetSocketAddress(getBroadcastAddress(localAddress.getHostString()), PORT); + receiveBuffer = ByteBuffer.allocate(BROADCAST_BUFFER_RECV_SIZE); + + setupOwnInfo(); + + try (var selector = Selector.open(); + DatagramChannel receiveChannel = DatagramChannel.open(StandardProtocolFamily.INET) + .setOption(StandardSocketOptions.SO_REUSEADDR, true) + .bind(new InetSocketAddress(localAddress.getHostString(), PORT)); + DatagramChannel sendChannel = DatagramChannel.open(StandardProtocolFamily.INET) + .setOption(StandardSocketOptions.SO_BROADCAST, true) + .bind(new InetSocketAddress(localAddress.getHostString(), 0)) + ) + { + sendAddress = (InetSocketAddress) sendChannel.getLocalAddress(); + receiveChannel.configureBlocking(false); + receiveChannel.register(selector, SelectionKey.OP_READ); + state = State.BROADCASTING; + + while (true) + { + if (state == State.BROADCASTING) + { + updateOwnInfo(); + sendBroadcast(sendChannel); + } + + selector.select(getSelectorTimeout()); + if (Thread.interrupted()) + { + setState(State.INTERRUPTED); + break; + } + handleSelection(selector); + } + } + catch (ClosedByInterruptException e) + { + log.debug("Interrupted, bailing out..."); + } + catch (IOException e) + { + log.error("Error: ", e); + } + } + + private void handleSelection(Selector selector) + { + Iterator selectedKeys = selector.selectedKeys().iterator(); + + if (!selectedKeys.hasNext()) + { + // This was a timeout + setState(State.BROADCASTING); + return; + } + + while (selectedKeys.hasNext()) + { + try + { + SelectionKey key = selectedKeys.next(); + selectedKeys.remove(); + + if (!key.isValid()) + { + continue; + } + + if (key.isReadable()) + { + read(key); + } + } + catch (IOException e) + { + log.error("Glitch, continuing...", e); // XXX: I think I should keep that part in case there's a transient network error. need experimenting + } + } + + // We're past the timeout so send again + if (Duration.between(sent, Instant.now()).compareTo(BROADCAST_MAX_WAIT_TIME) > 0) + { + setState(State.BROADCASTING); + } + } + + private long getSelectorTimeout() + { + return switch (state) + { + case WAITING -> Math.max(BROADCAST_MAX_WAIT_TIME.toMillis() - Duration.between(sent, Instant.now()).toMillis(), 0L); + default -> 0L; + }; + } + + private void setState(State newState) + { + state = newState; + } + + private void read(SelectionKey key) throws IOException + { + assert state == State.WAITING; + + DatagramChannel channel = (DatagramChannel) key.channel(); + InetSocketAddress peerAddress = (InetSocketAddress) channel.receive(receiveBuffer); + receiveBuffer.flip(); + + if (!peerAddress.equals(sendAddress)) // ignore our own packets + { + try + { + UdpDiscoveryPeer peer = UdpDiscoveryProtocol.parsePacket(receiveBuffer, peerAddress); + log.debug("Got peer: {}", peer); + + if (isValidPeer(peer)) + { + if (!peers.containsKey(peer.getPeerId())) // XXX: removed this because the original version always increments the packetIndex()... || peers.get(peer.getPeerId()).getPacketIndex() != peer.getPacketIndex()) // We use != so that it works with the 32-bit version + { + // Add or update + peers.put(peer.getPeerId(), peer); + try (var session = new DatabaseSession(databaseSessionManager)) + { + log.debug("Trying to update friend's IP"); + + locationService.findLocationById(peer.getLocationId()).ifPresent(location -> { + if (!location.isConnected()) + { + var lanConnection = Connection.from(PeerAddress.from(peer.getIpAddress(), peer.getLocalPort())); + location.getConnections().stream() + .filter(connection -> connection.equals(lanConnection)) + .findFirst() + .ifPresentOrElse(connection -> { + }, () -> { + log.debug("Updating friend {} with ip {}", location, lanConnection); + location.addConnection(lanConnection); + }); + } + } + ); + } + } + // XXX: and if a client is missing for 10 broadcasts or so, remove it + } + } + catch (RuntimeException e) + { + log.debug("Failed to parse packet: {}", e.getMessage()); + } + } + receiveBuffer.clear(); + } + + private void sendBroadcast(DatagramChannel channel) throws IOException + { + assert state == State.BROADCASTING; + + channel.send(sendBuffer, broadcastAddress); + sent = Instant.now(); + setState(State.WAITING); + sendBuffer.rewind(); + } + + private boolean isValidPeer(UdpDiscoveryPeer peer) + { + return peer.getAppId() == APP_ID && peer.getStatus() == UdpDiscoveryPeer.Status.PRESENT; + } +} diff --git a/app/src/main/java/io/xeres/app/net/bdisc/UdpDiscoveryPeer.java b/app/src/main/java/io/xeres/app/net/bdisc/UdpDiscoveryPeer.java new file mode 100644 index 000000000..8f8af1557 --- /dev/null +++ b/app/src/main/java/io/xeres/app/net/bdisc/UdpDiscoveryPeer.java @@ -0,0 +1,149 @@ +/* + * Copyright (c) 2019-2021 by David Gerber - https://zapek.com + * + * This file is part of Xeres. + * + * Xeres is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Xeres is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Xeres. If not, see . + */ + +package io.xeres.app.net.bdisc; + +import io.xeres.common.id.LocationId; +import io.xeres.common.id.ProfileFingerprint; + +public class UdpDiscoveryPeer +{ + public enum Status + { + PRESENT, + LEAVING // Not implemented. I don't see the point + } + + private Status status; + private int appId; + private int peerId; + private long packetIndex; + private String ipAddress; + + private ProfileFingerprint fingerprint; + private LocationId locationId; + private int localPort; + private String profileName; + + public Status getStatus() + { + return status; + } + + public void setStatus(Status status) + { + this.status = status; + } + + public int getAppId() + { + return appId; + } + + public void setAppId(int appId) + { + this.appId = appId; + } + + public int getPeerId() + { + return peerId; + } + + public void setPeerId(int peerId) + { + this.peerId = peerId; + } + + public long getPacketIndex() + { + return packetIndex; + } + + public void setPacketIndex(long packetIndex) + { + this.packetIndex = packetIndex; + } + + public String getIpAddress() + { + return ipAddress; + } + + public void setIpAddress(String ipAddress) + { + this.ipAddress = ipAddress; + } + + public ProfileFingerprint getFingerprint() + { + return fingerprint; + } + + public void setFingerprint(ProfileFingerprint fingerprint) + { + this.fingerprint = fingerprint; + } + + public LocationId getLocationId() + { + return locationId; + } + + public void setLocationId(LocationId locationId) + { + this.locationId = locationId; + } + + public int getLocalPort() + { + return localPort; + } + + public void setLocalPort(int localPort) + { + this.localPort = localPort; + } + + public String getProfileName() + { + return profileName; + } + + public void setProfileName(String profileName) + { + this.profileName = profileName; + } + + @Override + public String toString() + { + return "UdpDiscoveryPeer{" + + "status=" + status + + ", AppId=" + appId + + ", peerId=" + peerId + + ", packetIndex=" + packetIndex + + ", fingerprint=" + fingerprint + + ", locationId=" + locationId + + ", ipAddress='" + ipAddress + '\'' + + ", localPort=" + localPort + + ", profileName='" + profileName + '\'' + + '}'; + } +} diff --git a/app/src/main/java/io/xeres/app/net/bdisc/UdpDiscoveryProtocol.java b/app/src/main/java/io/xeres/app/net/bdisc/UdpDiscoveryProtocol.java new file mode 100644 index 000000000..496290dcc --- /dev/null +++ b/app/src/main/java/io/xeres/app/net/bdisc/UdpDiscoveryProtocol.java @@ -0,0 +1,118 @@ +/* + * Copyright (c) 2019-2021 by David Gerber - https://zapek.com + * + * This file is part of Xeres. + * + * Xeres is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Xeres is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Xeres. If not, see . + */ + +package io.xeres.app.net.bdisc; + +import io.netty.buffer.ByteBuf; +import io.netty.buffer.Unpooled; +import io.xeres.app.xrs.serialization.Serializer; +import io.xeres.common.id.LocationId; +import io.xeres.common.id.ProfileFingerprint; + +import java.net.InetSocketAddress; +import java.nio.ByteBuffer; + +public final class UdpDiscoveryProtocol +{ + private static final int MAGIC = 0x524e3655; // RN6U + + private UdpDiscoveryProtocol() + { + throw new UnsupportedOperationException("Utility class"); + } + + public static UdpDiscoveryPeer parsePacket(ByteBuffer buffer, InetSocketAddress peerAddress) + { + if (buffer.limit() < 29) + { + throw new IllegalArgumentException("Buffer is too small: " + buffer.limit()); + } + + if (buffer.getInt() != MAGIC) + { + throw new IllegalArgumentException("Wrong magic number in header"); + } + + var peer = new UdpDiscoveryPeer(); + peer.setIpAddress(peerAddress.getAddress().getHostAddress()); + + buffer.getInt(); // reserved + peer.setStatus(UdpDiscoveryPeer.Status.values()[buffer.get()]); + peer.setAppId(buffer.getInt()); + peer.setPeerId(buffer.getInt()); + + // Because of https://github.com/truvorskameikin/udp-discovery-cpp/commit/d37a19f2326d2a44dff65c2ce26c7d19380ef699 the following + // either uses a 64-bit packetIndex or a 32-bit one + overflow byte. We read the padding ahead to figure out + // which version it is. + if (buffer.getShort(7) == 0) + { + peer.setPacketIndex(buffer.getInt()); // XXX: this one just increments all the time + buffer.get(); // padding + } + else + { + peer.setPacketIndex(buffer.getLong()); + } + + int userDataSize = buffer.getShort(); + int paddingSize = buffer.getShort(); + if (userDataSize > buffer.remaining()) + { + throw new IllegalArgumentException("Userdata size of " + userDataSize + " is too big (" + buffer.remaining() + " remaining)"); + } + + ByteBuf buf = Unpooled.wrappedBuffer(buffer); + peer.setFingerprint((ProfileFingerprint) Serializer.deserializeIdentifier(buf, ProfileFingerprint.class)); + peer.setLocationId((LocationId) Serializer.deserializeIdentifier(buf, LocationId.class)); + peer.setLocalPort(Serializer.deserializeShort(buf)); + peer.setProfileName(Serializer.deserializeString(buf)); + + return peer; + } + + public static ByteBuffer createPacket(int maxSize, UdpDiscoveryPeer.Status status, int appId, int peerId, int counter, ProfileFingerprint fingerprint, LocationId locationId, int localPort, String profileName) + { + var buffer = ByteBuffer.allocate(maxSize); + + buffer.putInt(MAGIC); + buffer.putInt(0); // reserved + + buffer.put((byte) status.ordinal()); + buffer.putInt(appId); + buffer.putInt(peerId); + + // XXX: now this sucks... what to put here? 32-bit or 64-bit?! For now we'll put the 32-bit + // version but ideally we should monitor what happens and use one or the other depending on + // what is seen. + buffer.putInt(counter); + buffer.put((byte) 0); + + ByteBuf buf = Unpooled.buffer(); + Serializer.serialize(buf, fingerprint); + Serializer.serialize(buf, locationId); + Serializer.serialize(buf, (short) localPort); + Serializer.serialize(buf, profileName); + + buffer.putShort((short) buf.writerIndex()); + buffer.putShort((short) 0); + buffer.put(buf.nioBuffer()); + + return buffer; + } +} diff --git a/app/src/main/java/io/xeres/app/net/dht/DHTService.java b/app/src/main/java/io/xeres/app/net/dht/DHTService.java new file mode 100644 index 000000000..c4f2744b0 --- /dev/null +++ b/app/src/main/java/io/xeres/app/net/dht/DHTService.java @@ -0,0 +1,220 @@ +/* + * Copyright (c) 2019-2021 by David Gerber - https://zapek.com + * + * This file is part of Xeres. + * + * Xeres is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Xeres is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Xeres. If not, see . + */ + +package io.xeres.app.net.dht; + +import io.xeres.app.configuration.DataDirConfiguration; +import io.xeres.common.protocol.ip.IP; +import lbms.plugins.mldht.DHTConfiguration; +import lbms.plugins.mldht.kad.*; +import lbms.plugins.mldht.kad.messages.MessageBase; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.stereotype.Service; + +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStreamReader; +import java.net.InetAddress; +import java.nio.file.Files; +import java.nio.file.Path; +import java.time.Duration; +import java.time.Instant; +import java.util.Objects; +import java.util.function.Predicate; + +import static lbms.plugins.mldht.kad.DHT.DHTtype.IPV4_DHT; +import static lbms.plugins.mldht.kad.DHT.LogLevel.Fatal; + +@Service +public class DHTService implements DHTStatusListener, DHTConfiguration, DHTStatsListener, DHT.IncomingMessageListener +{ + private static final Logger log = LoggerFactory.getLogger(DHTService.class); + + private static final String DHT_DATA_DIR = "dht"; + private static final Duration STATS_DELAY = Duration.ofMinutes(1); + + private DHT dht; + private int localPort; + + private Instant lastStats; + + private final DataDirConfiguration dataDirConfiguration; + + public DHTService(DataDirConfiguration dataDirConfiguration) + { + this.dataDirConfiguration = dataDirConfiguration; + } + + public void start(int localPort) + { + this.localPort = localPort; + + DHT.setLogger(new DHTSpringLog()); + DHT.setLogLevel(Fatal); + dht = new DHT(IPV4_DHT); + dht.addStatusListener(this); + dht.addStatsListener(this); + dht.addIncomingMessageListener(this); + lastStats = Instant.now(); + + try + { + dht.start(this); + addBootstrappingNodes(); + } + catch (IOException e) + { + log.error("Error while setting up DHT: {}", e.getMessage(), e); + } + + dht.getServerManager().awaitActiveServer(); // XXX: catch the completable future to get a RPCServer to work with + // see https://github.com/the8472/mldht/blob/master/docs/use-as-library.md + } + + public void stop() + { + if (dht != null && dht.isRunning()) + { + dht.stop(); + } + } + + @Override + public void statusChanged(DHTStatus newStatus, DHTStatus oldStatus) + { + switch (newStatus) + { + case Running -> log.info("DHT status -> running"); + + // XXX: wait for that event before making us as usable + case Stopped -> log.info("DHT status -> stopped"); + + // XXX: allow to wait on that while shutting down + case Initializing -> log.info("DHT status -> initializing"); + } + } + + @Override + public boolean isPersistingID() + { + return false; + } + + @Override + public Path getStoragePath() + { + var path = Path.of(dataDirConfiguration.getDataDir(), DHT_DATA_DIR); + if (Files.notExists(path)) + { + try + { + Files.createDirectory(path); + } + catch (IOException e) + { + log.error("Failed to create DHT storage directory: {}, storage disabled", e.getMessage()); + return null; + } + } + return path; + } + + @Override + public int getListeningPort() + { + return localPort; + } + + @Override + public boolean noRouterBootstrap() + { + return true; + } + + @Override + public boolean allowMultiHoming() + { + return false; + } + + @Override + public Predicate filterBindAddress() + { + return address -> IP.isRoutableIp(address.getHostAddress()); + } + + @Override + public void statsUpdated(DHTStats dhtStats) + { + var now = Instant.now(); + + if (Duration.between(lastStats, now).compareTo(STATS_DELAY) > 0) + { + log.debug("Num peers: {}, recv pkt: {} ({} KB), sent pkt: {} ({} KB), keys: {}, items: {}", + dhtStats.getNumPeers(), + dhtStats.getNumReceivedPackets(), + dhtStats.getRpcStats().getReceivedBytes() / 1024, + dhtStats.getNumSentPackets(), + dhtStats.getRpcStats().getSentBytes() / 1024, + dhtStats.getDbStats().getKeyCount(), + dhtStats.getDbStats().getItemCount()); + + lastStats = now; + } + } + + @Override + public void received(DHT dht, MessageBase messageBase) + { + // XXX: handle messages here. called from message processing thread so must be non blocking and thread safe + } + + private void addBootstrappingNodes() + { + var reader = new BufferedReader(new InputStreamReader(Objects.requireNonNull(this.getClass().getResourceAsStream("/bdboot.txt")))); + var line = ""; + + try + { + while (reader.ready()) + { + line = reader.readLine(); + String[] tokens = line.split(" "); + String ip = tokens[0]; + var port = Integer.parseInt(tokens[1]); + + if (!IP.isRoutableIp(ip)) + { + throw new IllegalArgumentException("IP is invalid"); + } + if (!IP.isValidPort(port)) + { + throw new IllegalArgumentException("Port is invalid"); + } + log.debug("adding node {}:{}", ip, port); + dht.addDHTNode(ip, port); + } + } + catch (IOException | IllegalArgumentException e) + { + log.warn("Couldn't parse ipport of line: {} ({})", line, e.getMessage()); + } + } +} diff --git a/app/src/main/java/io/xeres/app/net/dht/DHTSpringLog.java b/app/src/main/java/io/xeres/app/net/dht/DHTSpringLog.java new file mode 100644 index 000000000..69ea3004b --- /dev/null +++ b/app/src/main/java/io/xeres/app/net/dht/DHTSpringLog.java @@ -0,0 +1,83 @@ +/* + * Copyright (c) 2019-2021 by David Gerber - https://zapek.com + * + * This file is part of Xeres. + * + * Xeres is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Xeres is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Xeres. If not, see . + */ + +package io.xeres.app.net.dht; + +import lbms.plugins.mldht.kad.DHT.LogLevel; +import lbms.plugins.mldht.kad.DHTLogger; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +public class DHTSpringLog implements DHTLogger +{ + private static final Logger log = LoggerFactory.getLogger(DHTSpringLog.class); + + private static final String EXCEPTION_HEADING = "Exception : "; + + @Override + public void log(String s, LogLevel logLevel) + { + switch (logLevel) + { + case Fatal -> log.error(s); + case Error -> log.warn(s); + case Info -> log.info(s); + case Debug -> log.debug(s); + case Verbose -> log.trace(s); + } + } + + @Override + public void log(Throwable throwable, LogLevel logLevel) + { + switch (logLevel) + { + case Fatal -> log.error(EXCEPTION_HEADING, throwable); + case Error -> log.warn(EXCEPTION_HEADING, throwable); + case Info -> log.info(EXCEPTION_HEADING, throwable); + case Debug -> log.debug(EXCEPTION_HEADING, throwable); + case Verbose -> log.trace(EXCEPTION_HEADING, throwable); + } + } + + public static LogLevel getLogLevel() + { + if (log.isTraceEnabled()) + { + return LogLevel.Verbose; + } + else if (log.isDebugEnabled()) + { + return LogLevel.Debug; + } + else if (log.isInfoEnabled()) + { + return LogLevel.Info; + } + else if (log.isWarnEnabled()) + { + return LogLevel.Error; + } + else if (log.isErrorEnabled()) + { + return LogLevel.Fatal; + } + return LogLevel.Info; + } +} diff --git a/app/src/main/java/io/xeres/app/net/dht/package-info.java b/app/src/main/java/io/xeres/app/net/dht/package-info.java new file mode 100644 index 000000000..02833513b --- /dev/null +++ b/app/src/main/java/io/xeres/app/net/dht/package-info.java @@ -0,0 +1,28 @@ +/* + * Copyright (c) 2019-2020 by David Gerber - https://zapek.com + * + * This file is part of Xeres. + * + * Xeres is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Xeres is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Xeres. If not, see . + */ + +/** + * DHT implementation. Note: + *
    + *
  • RS uses the DHT of Bittorrent
  • + *
  • it has some limitations regarding what can be put in the metadata
  • + *
  • they want to switch to a better one at some point
  • + *
+ */ +package io.xeres.app.net.dht; diff --git a/app/src/main/java/io/xeres/app/net/peer/ConnectionDirection.java b/app/src/main/java/io/xeres/app/net/peer/ConnectionDirection.java new file mode 100644 index 000000000..5ec4a256a --- /dev/null +++ b/app/src/main/java/io/xeres/app/net/peer/ConnectionDirection.java @@ -0,0 +1,26 @@ +/* + * Copyright (c) 2019-2021 by David Gerber - https://zapek.com + * + * This file is part of Xeres. + * + * Xeres is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Xeres is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Xeres. If not, see . + */ + +package io.xeres.app.net.peer; + +public enum ConnectionDirection +{ + INCOMING, + OUTGOING +} diff --git a/app/src/main/java/io/xeres/app/net/peer/PeerAttribute.java b/app/src/main/java/io/xeres/app/net/peer/PeerAttribute.java new file mode 100644 index 000000000..053c22174 --- /dev/null +++ b/app/src/main/java/io/xeres/app/net/peer/PeerAttribute.java @@ -0,0 +1,33 @@ +/* + * Copyright (c) 2019-2021 by David Gerber - https://zapek.com + * + * This file is part of Xeres. + * + * Xeres is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Xeres is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Xeres. If not, see . + */ + +package io.xeres.app.net.peer; + +import io.netty.util.AttributeKey; + +public final class PeerAttribute +{ + public static final AttributeKey MULTI_PACKET = AttributeKey.valueOf("MULTI_PACKET"); + public static final AttributeKey PEER_CONNECTION = AttributeKey.valueOf("PEER_CONNECTION"); + + private PeerAttribute() + { + throw new UnsupportedOperationException("Utility class"); + } +} diff --git a/app/src/main/java/io/xeres/app/net/peer/PeerConnection.java b/app/src/main/java/io/xeres/app/net/peer/PeerConnection.java new file mode 100644 index 000000000..31eb93af1 --- /dev/null +++ b/app/src/main/java/io/xeres/app/net/peer/PeerConnection.java @@ -0,0 +1,136 @@ +/* + * Copyright (c) 2019-2020 by David Gerber - https://zapek.com + * + * This file is part of Xeres. + * + * Xeres is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Xeres is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Xeres. If not, see . + */ + +package io.xeres.app.net.peer; + +import io.netty.channel.ChannelHandlerContext; +import io.netty.util.concurrent.ScheduledFuture; +import io.xeres.app.database.model.location.Location; +import io.xeres.app.xrs.service.RsService; + +import java.util.*; +import java.util.concurrent.TimeUnit; + +public class PeerConnection +{ + private Location location; + private final ChannelHandlerContext ctx; + private final Set services = new HashSet<>(); + private boolean servicesSent; + private final Map> serviceData = new HashMap<>(); + private final List> schedules = new ArrayList<>(); + + public PeerConnection(Location location, ChannelHandlerContext ctx) + { + this.location = location; + this.ctx = ctx; + } + + public ChannelHandlerContext getCtx() + { + return ctx; + } + + public Location getLocation() + { + return location; + } + + public void updateLocation(Location location) + { + this.location = location; + } + + public void addService(RsService service) + { + services.add(service); + } + + public boolean isServiceSupported(RsService rsService) + { + return services.contains(rsService); + } + + public boolean hasSentServices() + { + return servicesSent; + } + + public void setServicesSent() + { + servicesSent = true; + } + + public void putServiceData(RsService service, int key, Object data) + { + Map map = serviceData.getOrDefault(service.getServiceType().getType(), new HashMap<>()); + map.put(key, data); + serviceData.put(service.getServiceType().getType(), map); + } + + public Optional getServiceData(RsService service, int key) + { + Map serviceMap = serviceData.get(service.getServiceType().getType()); + if (serviceMap == null) + { + return Optional.empty(); + } + return Optional.ofNullable(serviceMap.get(key)); + } + + public ScheduledFuture scheduleAtFixedRate(Runnable command, long initialDelay, long period, TimeUnit unit) + { + ScheduledFuture scheduledFuture = ctx.executor().scheduleAtFixedRate(command, initialDelay, period, unit); + schedules.add(scheduledFuture); + return scheduledFuture; + } + + public ScheduledFuture scheduleWithFixedDelay(Runnable command, long initialDelay, long delay, TimeUnit unit) + { + ScheduledFuture scheduledFuture = ctx.executor().scheduleWithFixedDelay(command, initialDelay, delay, unit); + schedules.add(scheduledFuture); + return scheduledFuture; + } + + public ScheduledFuture schedule(Runnable command, long delay, TimeUnit unit) + { + ScheduledFuture scheduledFuture = ctx.executor().schedule(command, delay, unit); + schedules.add(scheduledFuture); + return scheduledFuture; + } + + public void shutdown() + { + services.forEach(RsService::shutdown); + } + + public void cleanup() + { + schedules.forEach(scheduledFuture -> scheduledFuture.cancel(false)); + } + + @Override + public String toString() + { + return "PeerConnection{" + + "location=" + location + + ", ip=" + ctx.channel().remoteAddress() + + '}'; + } +} diff --git a/app/src/main/java/io/xeres/app/net/peer/PeerConnectionManager.java b/app/src/main/java/io/xeres/app/net/peer/PeerConnectionManager.java new file mode 100644 index 000000000..8986bfdc9 --- /dev/null +++ b/app/src/main/java/io/xeres/app/net/peer/PeerConnectionManager.java @@ -0,0 +1,189 @@ +/* + * Copyright (c) 2019-2021 by David Gerber - https://zapek.com + * + * This file is part of Xeres. + * + * Xeres is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Xeres is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Xeres. If not, see . + */ + +package io.xeres.app.net.peer; + +import io.netty.buffer.ByteBuf; +import io.netty.buffer.Unpooled; +import io.netty.channel.ChannelFuture; +import io.netty.channel.ChannelHandlerContext; +import io.xeres.app.database.model.location.Location; +import io.xeres.app.xrs.item.Item; +import io.xeres.app.xrs.serialization.SerializationFlags; +import io.xeres.app.xrs.service.RsService; +import io.xeres.common.id.Identifier; +import io.xeres.common.message.MessageType; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.messaging.simp.SimpMessageSendingOperations; +import org.springframework.stereotype.Component; + +import java.util.EnumSet; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; +import java.util.function.Consumer; + +import static io.xeres.app.net.peer.PeerAttribute.PEER_CONNECTION; +import static io.xeres.common.message.MessageHeaders.buildMessageHeaders; + +@Component +public class PeerConnectionManager +{ + private static final Logger log = LoggerFactory.getLogger(PeerConnectionManager.class); + + private final SimpMessageSendingOperations messagingTemplate; + + private final Map peers = new ConcurrentHashMap<>(); + + public PeerConnectionManager(SimpMessageSendingOperations messagingTemplate) + { + this.messagingTemplate = messagingTemplate; + } + + public PeerConnection addPeer(Location location, ChannelHandlerContext ctx) + { + if (peers.containsKey(location.getId())) + { + throw new IllegalStateException("Location " + location + " added already"); + } + var peerConnection = new PeerConnection(location, ctx); + peers.put(location.getId(), peerConnection); + ctx.channel().attr(PEER_CONNECTION).set(peerConnection); + return peerConnection; + } + + public void updatePeer(Location location) + { + if (!peers.containsKey(location.getId())) + { + throw new IllegalStateException("Location " + location + " is not in the list of peers"); + } + peers.get(location.getId()).updateLocation(location); + } + + public void removePeer(Location location) + { + if (!peers.containsKey(location.getId())) + { + throw new IllegalStateException("Location " + location + " is not in the list of peers"); + } + peers.remove(location.getId()); + } + + public void shutdown() + { + peers.forEach((id, peerConnection) -> peerConnection.shutdown()); + } + + public ChannelFuture writeItem(Location location, Item item, RsService rsService) + { + PeerConnection peer = peers.get(location.getId()); + if (peer != null) + { + return writeItem(peer, item, rsService); + } + log.warn("Peer with location {} not found while trying to write item. User disconnected?", location); + return null; // XXX: use executor.newFailedFuture()? but where do I get the executor from? + } + + public ChannelFuture writeItem(PeerConnection peerConnection, Item item, RsService rsService) + { + item.setOutgoing(peerConnection.getCtx().alloc(), 2, rsService.getServiceType(), rsService.getItemSubtype(item)); + return writeItem(peerConnection.getCtx(), item); + } + + /** + * Serializes an item to make a signature out of it. + * + * @param item the item + * @param rsService the service + * @return a ByteBuf. Don't forget to release() it once you're done + */ + public ByteBuf serializeItemForSignature(Item item, RsService rsService) + { + item.setOutgoing(Unpooled.buffer().alloc(), 2, rsService.getServiceType(), rsService.getItemSubtype(item)); + return item.serializeItem(EnumSet.of(SerializationFlags.SIGNATURE)).getBuffer(); + } + + @Deprecated(forRemoval = true) // use doForAllPeers() + public void writeItemToAllPeers(Item item, RsService rsService) + { + peers.forEach((aLong, peerConnection) -> + { + if (peerConnection.isServiceSupported(rsService)) + { + writeItem(peerConnection, item, rsService); + } + }); + } + + @Deprecated(forRemoval = true) // use doForAllPeers() + public void writeItemToAllPeersExceptSender(PeerConnection senderPeerConnection, Item item, RsService rsService) + { + peers.forEach((peerId, peerConnection) -> + { + if (peerConnection.isServiceSupported(rsService) && !senderPeerConnection.equals(peerConnection)) + { + writeItem(peerConnection, item, rsService); + } + }); + } + + public void doForAllPeers(Consumer action, RsService rsService) + { + peers.forEach((peerId, peerConnection) -> + { + if (peerConnection.isServiceSupported(rsService)) + { + action.accept(peerConnection); + } + }); + } + + public static ChannelFuture writeItem(ChannelHandlerContext ctx, Item item) + { + var rawItem = item.serializeItem(EnumSet.noneOf(SerializationFlags.class)); + log.debug("==> {}", item); + log.trace("Message content: {}", rawItem); + return ctx.writeAndFlush(rawItem); + } + + public void sendToSubscriptions(String path, MessageType type, Object payload) + { + Map headers = buildMessageHeaders(type); + sendToSubscriptions(path, headers, payload); + } + + public void sendToSubscriptions(String path, MessageType type, long destination, Object payload) + { + Map headers = buildMessageHeaders(type, String.valueOf(destination)); + sendToSubscriptions(path, headers, payload); + } + + public void sendToSubscriptions(String path, MessageType type, Identifier destination, Object payload) + { + Map headers = buildMessageHeaders(type, destination.toString()); + sendToSubscriptions(path, headers, payload); + } + + public void sendToSubscriptions(String path, Map headers, Object payload) + { + messagingTemplate.convertAndSend(path, payload, headers); + } +} diff --git a/app/src/main/java/io/xeres/app/net/peer/bootstrap/PeerClient.java b/app/src/main/java/io/xeres/app/net/peer/bootstrap/PeerClient.java new file mode 100644 index 000000000..c7eebeed7 --- /dev/null +++ b/app/src/main/java/io/xeres/app/net/peer/bootstrap/PeerClient.java @@ -0,0 +1,120 @@ +/* + * Copyright (c) 2019-2020 by David Gerber - https://zapek.com + * + * This file is part of Xeres. + * + * Xeres is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Xeres is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Xeres. If not, see . + */ + +package io.xeres.app.net.peer.bootstrap; + +import io.netty.bootstrap.Bootstrap; +import io.netty.channel.ChannelFuture; +import io.netty.channel.EventLoopGroup; +import io.netty.channel.nio.NioEventLoopGroup; +import io.netty.channel.socket.nio.NioSocketChannel; +import io.netty.util.concurrent.EventExecutorGroup; +import io.xeres.app.database.DatabaseSessionManager; +import io.xeres.app.net.peer.PeerConnectionManager; +import io.xeres.app.net.protocol.PeerAddress; +import io.xeres.app.properties.NetworkProperties; +import io.xeres.app.service.LocationService; +import io.xeres.app.service.PrefsService; +import io.xeres.app.xrs.service.serviceinfo.ServiceInfoService; +import io.xeres.common.properties.StartupProperties; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.stereotype.Component; + +import javax.net.ssl.SSLException; +import java.security.NoSuchAlgorithmException; +import java.security.spec.InvalidKeySpecException; + +import static io.xeres.app.net.peer.ConnectionDirection.OUTGOING; + +@Component +public class PeerClient +{ + private static final Logger log = LoggerFactory.getLogger(PeerClient.class); + + private final PrefsService prefsService; + private final NetworkProperties networkProperties; + private final LocationService locationService; + private final PeerConnectionManager peerConnectionManager; + private final DatabaseSessionManager databaseSessionManager; + private final ServiceInfoService serviceInfoService; + + private Bootstrap bootstrap; + private EventLoopGroup group; + + public PeerClient(PrefsService prefsService, NetworkProperties networkProperties, LocationService locationService, PeerConnectionManager peerConnectionManager, DatabaseSessionManager databaseSessionManager, ServiceInfoService serviceInfoService) + { + this.prefsService = prefsService; + this.networkProperties = networkProperties; + this.locationService = locationService; + this.peerConnectionManager = peerConnectionManager; + this.databaseSessionManager = databaseSessionManager; + this.serviceInfoService = serviceInfoService; + } + + public void start(EventExecutorGroup sslExecutorGroup, EventExecutorGroup eventExecutorGroup) + { + group = new NioEventLoopGroup(); + + try + { + bootstrap = new Bootstrap(); + bootstrap.group(group) + .channel(NioSocketChannel.class) + .handler(new PeerInitializer(peerConnectionManager, databaseSessionManager, locationService, prefsService, sslExecutorGroup, eventExecutorGroup, networkProperties, serviceInfoService, OUTGOING)); + } + catch (SSLException | NoSuchAlgorithmException | InvalidKeySpecException e) + { + log.error("Error setting up PeerClient: {}", e.getMessage()); + } + } + + public void stop() + { + if (group == null) + { + return; + } + + if (StartupProperties.getBoolean(StartupProperties.Property.FAST_SHUTDOWN, false)) + { + log.debug("Shutting down netty client (fast)..."); + group.shutdownGracefully(); + } + else + { + log.info("Shutting down netty client..."); + try + { + group.shutdownGracefully().sync(); + } + catch (InterruptedException e) + { + log.error("Error while shutting down netty client: {}", e.getMessage()); + Thread.currentThread().interrupt(); + } + } + } + + public ChannelFuture connect(PeerAddress peerAddress) + { + assert group != null; + return bootstrap.connect(peerAddress.getSocketAddress()); + } +} diff --git a/app/src/main/java/io/xeres/app/net/peer/bootstrap/PeerInitializer.java b/app/src/main/java/io/xeres/app/net/peer/bootstrap/PeerInitializer.java new file mode 100644 index 000000000..ff992cb10 --- /dev/null +++ b/app/src/main/java/io/xeres/app/net/peer/bootstrap/PeerInitializer.java @@ -0,0 +1,107 @@ +/* + * Copyright (c) 2019-2021 by David Gerber - https://zapek.com + * + * This file is part of Xeres. + * + * Xeres is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Xeres is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Xeres. If not, see . + */ + +package io.xeres.app.net.peer.bootstrap; + +import io.netty.channel.ChannelHandler; +import io.netty.channel.ChannelInitializer; +import io.netty.channel.ChannelPipeline; +import io.netty.channel.socket.SocketChannel; +import io.netty.handler.ssl.SslContext; +import io.netty.handler.timeout.IdleStateHandler; +import io.netty.util.concurrent.EventExecutorGroup; +import io.xeres.app.database.DatabaseSessionManager; +import io.xeres.app.net.peer.ConnectionDirection; +import io.xeres.app.net.peer.PeerConnectionManager; +import io.xeres.app.net.peer.pipeline.*; +import io.xeres.app.net.peer.ssl.SSL; +import io.xeres.app.properties.NetworkProperties; +import io.xeres.app.service.LocationService; +import io.xeres.app.service.PrefsService; +import io.xeres.app.xrs.service.serviceinfo.ServiceInfoService; + +import javax.net.ssl.SSLException; +import java.security.NoSuchAlgorithmException; +import java.security.spec.InvalidKeySpecException; +import java.time.Duration; + +public class PeerInitializer extends ChannelInitializer +{ + public static final Duration PEER_IDLE_TIMEOUT = Duration.ofMinutes(2); /* peers not responding during that time are considered dead */ + public static final Duration ACTIVITY_PROD = Duration.ofMinutes(1); /* if idle, sends a prod activity after that time */ + + private final SslContext sslContext; + private final ConnectionDirection direction; + + private final EventExecutorGroup sslExecutorGroup; + private final EventExecutorGroup eventExecutorGroup; + + private final NetworkProperties networkProperties; + private final LocationService locationService; + private final PeerConnectionManager peerConnectionManager; + private final DatabaseSessionManager databaseSessionManager; + private final ServiceInfoService serviceInfoService; + + private static final ChannelHandler SIMPLE_PACKET_ENCODER = new SimplePacketEncoder(); + private static final ChannelHandler ITEM_ENCODER = new ItemEncoder(); + private static final ChannelHandler IDLE_EVENT_HANDLER = new IdleEventHandler(PEER_IDLE_TIMEOUT); + + public PeerInitializer(PeerConnectionManager peerConnectionManager, DatabaseSessionManager databaseSessionManager, LocationService locationService, PrefsService prefsService, EventExecutorGroup sslExecutorGroup, EventExecutorGroup eventExecutorGroup, NetworkProperties networkProperties, ServiceInfoService serviceInfoService, ConnectionDirection direction) throws InvalidKeySpecException, NoSuchAlgorithmException, SSLException + { + this.serviceInfoService = serviceInfoService; + this.sslContext = SSL.createSslContext(prefsService.getLocationPrivateKeyData(), prefsService.getLocationCertificate(), direction); + this.sslExecutorGroup = sslExecutorGroup; + this.eventExecutorGroup = eventExecutorGroup; + this.networkProperties = networkProperties; + this.locationService = locationService; + this.peerConnectionManager = peerConnectionManager; + this.databaseSessionManager = databaseSessionManager; + this.direction = direction; + } + + @Override + protected void initChannel(SocketChannel channel) + { + ChannelPipeline pipeline = channel.pipeline(); + + // Build the pipeline in order. + // Inbound + // vvvvvvv + + // add SSL to encrypt and decrypt everything + pipeline.addLast(sslExecutorGroup, sslContext.newHandler(channel.alloc())); + + // decoder (inbound) + pipeline.addLast(new PacketDecoder()); + pipeline.addLast(new ItemDecoder()); + + // encoder (outbound) + pipeline.addLast(networkProperties.isPacketSlicing() ? new MultiPacketEncoder() : SIMPLE_PACKET_ENCODER); + pipeline.addLast(ITEM_ENCODER); + + // business logic + pipeline.addLast(new IdleStateHandler((int) PEER_IDLE_TIMEOUT.toSeconds(), (int) ACTIVITY_PROD.toSeconds(), 0)); + pipeline.addLast(IDLE_EVENT_HANDLER); + + // ^^^^^^^^ + // Outbound + + pipeline.addLast(eventExecutorGroup, new PeerHandler(locationService, peerConnectionManager, databaseSessionManager, serviceInfoService, direction)); + } +} diff --git a/app/src/main/java/io/xeres/app/net/peer/bootstrap/PeerServer.java b/app/src/main/java/io/xeres/app/net/peer/bootstrap/PeerServer.java new file mode 100644 index 000000000..0cdb3def7 --- /dev/null +++ b/app/src/main/java/io/xeres/app/net/peer/bootstrap/PeerServer.java @@ -0,0 +1,143 @@ +/* + * Copyright (c) 2019-2020 by David Gerber - https://zapek.com + * + * This file is part of Xeres. + * + * Xeres is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Xeres is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Xeres. If not, see . + */ + +package io.xeres.app.net.peer.bootstrap; + +import io.netty.bootstrap.ServerBootstrap; +import io.netty.channel.ChannelFuture; +import io.netty.channel.ChannelOption; +import io.netty.channel.EventLoopGroup; +import io.netty.channel.nio.NioEventLoopGroup; +import io.netty.channel.socket.nio.NioServerSocketChannel; +import io.netty.handler.logging.LogLevel; +import io.netty.handler.logging.LoggingHandler; +import io.netty.util.concurrent.EventExecutorGroup; +import io.xeres.app.database.DatabaseSessionManager; +import io.xeres.app.net.peer.PeerConnectionManager; +import io.xeres.app.properties.NetworkProperties; +import io.xeres.app.service.LocationService; +import io.xeres.app.service.PrefsService; +import io.xeres.app.service.ProfileService; +import io.xeres.app.xrs.service.serviceinfo.ServiceInfoService; +import io.xeres.common.properties.StartupProperties; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.stereotype.Component; +import org.springframework.transaction.annotation.Transactional; + +import javax.net.ssl.SSLException; +import java.security.NoSuchAlgorithmException; +import java.security.spec.InvalidKeySpecException; + +import static io.xeres.app.net.peer.ConnectionDirection.INCOMING; + + +@Component +public class PeerServer +{ + private static final Logger log = LoggerFactory.getLogger(PeerServer.class); + + private final PrefsService prefsService; + private final ProfileService profileService; + private final NetworkProperties networkProperties; + private final LocationService locationService; + private final PeerConnectionManager peerConnectionManager; + private final DatabaseSessionManager databaseSessionManager; + private final ServiceInfoService serviceInfoService; + + private EventLoopGroup bossGroup; + private EventLoopGroup workerGroup; + private ChannelFuture channel; + + public PeerServer(PrefsService prefsService, ProfileService profileService, NetworkProperties networkProperties, LocationService locationService, PeerConnectionManager peerConnectionManager, DatabaseSessionManager databaseSessionManager, ServiceInfoService serviceInfoService) + { + this.prefsService = prefsService; + this.profileService = profileService; + this.networkProperties = networkProperties; + this.locationService = locationService; + this.peerConnectionManager = peerConnectionManager; + this.databaseSessionManager = databaseSessionManager; + this.serviceInfoService = serviceInfoService; + } + + @Transactional(readOnly = true) // needed for getPort() to work + public void start(EventExecutorGroup sslExecutorGroup, EventExecutorGroup eventExecutorGroup) + { + bossGroup = new NioEventLoopGroup(1); + workerGroup = new NioEventLoopGroup(); + + try + { + var serverBootstrap = new ServerBootstrap(); + serverBootstrap.group(bossGroup, workerGroup) + .channel(NioServerSocketChannel.class) + .option(ChannelOption.SO_BACKLOG, 128) // should be more + .option(ChannelOption.SO_REUSEADDR, true) + .handler(new LoggingHandler(LogLevel.DEBUG)) + .childHandler(new PeerInitializer(peerConnectionManager, databaseSessionManager, locationService, prefsService, sslExecutorGroup, eventExecutorGroup, networkProperties, serviceInfoService, INCOMING)); + + int port = getPort(); + channel = serverBootstrap.bind(port).sync(); // XXX: what if we cannot bind on the local port for some reasons because some other process uses it? investigate port ranges? + log.info("Listening on {}, port {}", channel.channel().localAddress(), port); + } + catch (SSLException | NoSuchAlgorithmException | InvalidKeySpecException e) + { + log.error("Error setting up PeerServer: {}", e.getMessage()); + } + catch (InterruptedException e) + { + log.error("Interrupted: {}", e.getMessage()); + Thread.currentThread().interrupt(); + } + } + + public void stop() + { + if (channel == null) + { + return; + } + + if (StartupProperties.getBoolean(StartupProperties.Property.FAST_SHUTDOWN, false)) + { + log.debug("Shutting down netty server (fast)..."); + workerGroup.shutdownGracefully(); + bossGroup.shutdownGracefully(); + } + else + { + log.info("Shutting down netty server..."); + try + { + workerGroup.shutdownGracefully().sync(); + bossGroup.shutdownGracefully().sync(); + } + catch (InterruptedException e) + { + log.error("Error while shutting down netty server: {}", e.getMessage()); + Thread.currentThread().interrupt(); + } + } + } + + private int getPort() + { + return profileService.getOwnProfile().getLocations().get(0).getConnections().get(0).getPort(); + } +} diff --git a/app/src/main/java/io/xeres/app/net/peer/packet/MultiPacket.java b/app/src/main/java/io/xeres/app/net/peer/packet/MultiPacket.java new file mode 100644 index 000000000..e7a789cb3 --- /dev/null +++ b/app/src/main/java/io/xeres/app/net/peer/packet/MultiPacket.java @@ -0,0 +1,175 @@ +/* + * Copyright (c) 2019-2021 by David Gerber - https://zapek.com + * + * This file is part of Xeres. + * + * Xeres is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Xeres is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Xeres. If not, see . + */ + +package io.xeres.app.net.peer.packet; + +import io.netty.buffer.ByteBuf; +import io.netty.channel.ChannelHandlerContext; + +import java.net.ProtocolException; +import java.util.List; + +/** + * This packet supports slicing and grouping for a more efficient + * transmission over an SSL link. + */ +public class MultiPacket extends Packet +{ + /** + * Maximum packet ID. Wraps around. + */ + public static final int MAXIMUM_ID = 16777216; + + /** + * Flag set for starting packets and full packets + * in the new format. + */ + public static final int SLICE_FLAG_START = 1; + + /** + * Flag set for ending packets and full packets + * in the new format. + */ + public static final int SLICE_FLAG_END = 2; + + private static final int HEADER_VERSION_INDEX = 0; + private static final int HEADER_FLAG_INDEX = 1; + private static final int HEADER_PACKET_ID_INDEX = 2; + public static final int HEADER_SIZE_INDEX = 6; + + protected static boolean isNewPacket(ByteBuf in) throws ProtocolException + { + if (in.getUnsignedByte(HEADER_VERSION_INDEX) == SLICE_PROTOCOL_VERSION_ID_01) + { + int id = (int) in.getUnsignedInt(HEADER_PACKET_ID_INDEX); + if (id >= MAXIMUM_ID || id < 0) + { + throw new ProtocolException("Illegal packet id (" + id + ")"); + } + return true; + } + return false; + } + + protected MultiPacket(ByteBuf in) + { + buf = in.retain(); + } + + public MultiPacket(ChannelHandlerContext ctx, List packets) + { + priority = packets.stream().findFirst().orElseThrow().getPriority(); + int size = packets.stream().mapToInt(Packet::getSize).sum(); + + buf = ctx.alloc().buffer(); // XXX: maybe we could use a CompoundBuffer (but maybe not because of the header in each packet), see documentation + writeHeader(SLICE_PROTOCOL_VERSION_ID_01, (byte) (SLICE_FLAG_START | SLICE_FLAG_END), 0, size); + packets.forEach(this::addPacket); + } + + private void addPacket(Packet packet) + { + buf.writeBytes(packet.getBuffer(), HEADER_SIZE, packet.getSize()); + packet.dispose(); + } + + private void writeHeader(int version, int flags, int packetId, int size) + { + buf.writeByte(version); + buf.writeByte(flags); + buf.writeInt(packetId); + buf.writeShort(size); + } + + public void setStart(boolean start) + { + addFlags(SLICE_FLAG_START); // XXX: why the boolean? + } + + public boolean isStart() + { + return (getFlags() & SLICE_FLAG_START) == SLICE_FLAG_START; + } + + public void setEnd(boolean end) + { + addFlags(SLICE_FLAG_END); // XXX: why the boolean? + } + + public boolean isEnd() + { + return (getFlags() & SLICE_FLAG_END) == SLICE_FLAG_END; + } + + public boolean isMiddle() + { + return !(isStart() || isEnd()); + } + + public boolean isSlice() + { + return !isComplete(); + } + + public void setId(int id) + { + buf.setInt(HEADER_PACKET_ID_INDEX, id); + } + + public int getId() + { + return (int) buf.getUnsignedInt(HEADER_PACKET_ID_INDEX); + } + + @Override + public int getSize() + { + return buf.getUnsignedShort(HEADER_SIZE_INDEX); + } + + @Override + public ByteBuf getItemBuffer() + { + return getBuffer().slice(HEADER_SIZE, getSize()); + } + + private int getFlags() + { + return buf.getUnsignedByte(HEADER_FLAG_INDEX); + } + + private void addFlags(int newFlags) + { + int currentFlags = buf.getUnsignedByte(HEADER_FLAG_INDEX); + currentFlags |= newFlags; + buf.setByte(HEADER_FLAG_INDEX, currentFlags); + } + + private void removeFlags(int newFlags) + { + int currentFlags = buf.getUnsignedByte(HEADER_FLAG_INDEX); + currentFlags &= ~newFlags; + buf.setByte(HEADER_FLAG_INDEX, currentFlags); + } + + @Override + public boolean isComplete() + { + return isStart() && isEnd(); + } +} diff --git a/app/src/main/java/io/xeres/app/net/peer/packet/Packet.java b/app/src/main/java/io/xeres/app/net/peer/packet/Packet.java new file mode 100644 index 000000000..a382cc7b5 --- /dev/null +++ b/app/src/main/java/io/xeres/app/net/peer/packet/Packet.java @@ -0,0 +1,152 @@ +/* + * Copyright (c) 2019-2021 by David Gerber - https://zapek.com + * + * This file is part of Xeres. + * + * Xeres is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Xeres is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Xeres. If not, see . + */ + +package io.xeres.app.net.peer.packet; + +import io.netty.buffer.ByteBuf; +import io.xeres.app.xrs.item.RawItem; + +import java.net.ProtocolException; +import java.util.Objects; + +public abstract class Packet implements Comparable +{ + /** + * Version of the packet protocol with slicing and grouping support. + * Also referred as new format. + */ + public static final int SLICE_PROTOCOL_VERSION_ID_01 = 16; + + /** + * Size of the header. Same for both packet protocols. + */ + public static final int HEADER_SIZE = 8; + + /** + * Optimal packet size for the new format. It fits better + * in the SSL encapsulation. + */ + public static final int OPTIMAL_PACKET_SIZE = 512; + + /** + * The maximum packet size, which is the buffer size per connection + * used by Retroshare actually. + */ + public static final int MAXIMUM_PACKET_SIZE = 262143; + + protected int priority = 3; + private int sequence; + + protected ByteBuf buf; + + public static Packet fromItem(RawItem rawItem) + { + Packet packet; + //if (rawItem.getPacketVersion() == 2) // this handles slice prods, which HAVE to use the old format, for now + //{ + // packet = new SimplePacket(rawItem.getBuffer()); + //} + //return new MultiPacket(item.getBuffer()); // XXX: when the encoder is ready + packet = new SimplePacket(rawItem.getBuffer()); + packet.setPriority(rawItem.getPriority()); + return packet; + } + + public static Packet fromBuffer(ByteBuf in) throws ProtocolException + { + return MultiPacket.isNewPacket(in) ? new MultiPacket(in) : new SimplePacket(in); + } + + protected Packet() + { + } + + public boolean isMulti() + { + return this instanceof MultiPacket; + } + + public abstract boolean isComplete(); + + public abstract int getSize(); + + public abstract ByteBuf getItemBuffer(); + + void setBuffer(ByteBuf buf) // XXX: for tests... check if it works well enough + { + this.buf = buf; + } + + public ByteBuf getBuffer() + { + return buf; + } + + public int getPriority() + { + return priority; + } + + public void setPriority(int priority) + { + this.priority = priority; + } + + public boolean isRealtimePriority() + { + return priority == 9; // XXX: make it nicer + } + + public void setSequence(int sequence) // XXX: possibly in new packets only. not sure though + { + this.sequence = sequence; + } + + public void dispose() + { + buf.release(); + } + + @Override + public boolean equals(Object o) + { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + var packet = (Packet) o; + return priority == packet.priority && sequence == packet.sequence && buf.equals(packet.buf); + } + + @Override + public int hashCode() + { + return Objects.hash(priority, sequence, buf); + } + + @Override + public int compareTo(Packet o) + { + int res = getPriority() - o.getPriority(); + + if (res == 0 && o != this) + { + res = o.sequence - sequence; + } + return res; + } +} diff --git a/app/src/main/java/io/xeres/app/net/peer/packet/SimplePacket.java b/app/src/main/java/io/xeres/app/net/peer/packet/SimplePacket.java new file mode 100644 index 000000000..b19aa5e3d --- /dev/null +++ b/app/src/main/java/io/xeres/app/net/peer/packet/SimplePacket.java @@ -0,0 +1,57 @@ +/* + * Copyright (c) 2019-2020 by David Gerber - https://zapek.com + * + * This file is part of Xeres. + * + * Xeres is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Xeres is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Xeres. If not, see . + */ + +package io.xeres.app.net.peer.packet; + +import io.netty.buffer.ByteBuf; + +/** + * This is the old packet format of RS. It is still + * used by RS in some cases (ie. transmission of a single small packet). + */ +public class SimplePacket extends Packet +{ + private static final int HEADER_VERSION_INDEX = 0; + private static final int HEADER_SERVICE_INDEX = 1; + private static final int HEADER_SUBPACKET_INDEX = 3; + public static final int HEADER_SIZE_INDEX = 4; + + protected SimplePacket(ByteBuf in) + { + buf = in.retain(); + } + + @Override + public int getSize() + { + return (int) buf.getUnsignedInt(HEADER_SIZE_INDEX); + } + + @Override + public ByteBuf getItemBuffer() + { + return getBuffer(); + } + + @Override + public boolean isComplete() + { + return true; + } +} diff --git a/app/src/main/java/io/xeres/app/net/peer/packet/package-info.java b/app/src/main/java/io/xeres/app/net/peer/packet/package-info.java new file mode 100644 index 000000000..30fef30f8 --- /dev/null +++ b/app/src/main/java/io/xeres/app/net/peer/packet/package-info.java @@ -0,0 +1,43 @@ +/* + * Copyright (c) 2019-2020 by David Gerber - https://zapek.com + * + * This file is part of Xeres. + * + * Xeres is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Xeres is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Xeres. If not, see . + */ + +/** + *

There are 2 header formats for Packets: + *

Old (describing a SimplePacket):
+ *

+ * +---------+---------+-----------+--------------------------------------+
+ * | version | service | subpacket | size, including header of 8 bytes    |
+ * +---------+---------+-----------+--------------------------------------+
+ * | 1 byte  | 2 bytes |   1 byte  |                4 bytes               |
+ * +---------+---------+-----------+--------------------------------------+
+ * 
+ *

New (describing a MultiPacket, version is always 16):
+ *

+ * +---------+---------+------------+--------------------------------------+
+ * | version |  flags  | packet id  | size, excluding header of 8 bytes    |
+ * +---------+---------+------------+--------------------------------------+
+ * | 1 byte  | 1 byte  |   4 bytes  |               2 bytes                |
+ * +---------+---------+------------+--------------------------------------+
+ * 
+ *

+ * Checking the protocol version (16) is enough to know if it's a new packet format. The simple packet + * format is just the Item. The multi packet format is basically the slicing header and the Item as data. It + * allows grouping and slicing to fit better into 512 bytes long data packets. + */ +package io.xeres.app.net.peer.packet; diff --git a/app/src/main/java/io/xeres/app/net/peer/pipeline/IdleEventHandler.java b/app/src/main/java/io/xeres/app/net/peer/pipeline/IdleEventHandler.java new file mode 100644 index 000000000..29f19038a --- /dev/null +++ b/app/src/main/java/io/xeres/app/net/peer/pipeline/IdleEventHandler.java @@ -0,0 +1,66 @@ +/* + * Copyright (c) 2019-2020 by David Gerber - https://zapek.com + * + * This file is part of Xeres. + * + * Xeres is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Xeres is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Xeres. If not, see . + */ + +package io.xeres.app.net.peer.pipeline; + +import io.netty.channel.ChannelHandler; +import io.netty.channel.ChannelHandlerContext; +import io.netty.channel.SimpleUserEventChannelHandler; +import io.netty.handler.timeout.IdleState; +import io.netty.handler.timeout.IdleStateEvent; +import io.xeres.app.net.peer.PeerConnectionManager; +import io.xeres.app.xrs.service.sliceprobe.item.SliceProbeItem; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.time.Duration; + +/** + * Event handler that automatically closes the connection if the peer doesn't send anything + * during a certain time. We also send a SliceProbeItem if we're idle ourselves (which is unlikely + * to happen during normal operations (ie. RTT and heartbeat services). + */ +@ChannelHandler.Sharable +public class IdleEventHandler extends SimpleUserEventChannelHandler +{ + private static final Logger log = LoggerFactory.getLogger(IdleEventHandler.class); + + private final Duration timeout; + + public IdleEventHandler(Duration timeout) + { + super(); + this.timeout = timeout; + } + + @Override + protected void eventReceived(ChannelHandlerContext ctx, IdleStateEvent evt) + { + if (evt.state() == IdleState.READER_IDLE) + { + log.info("No activity for {} seconds, closing channel of {}", timeout.toSeconds(), ctx.channel().remoteAddress()); + ctx.close(); + } + else if (evt.state() == IdleState.WRITER_IDLE) + { + log.info("Sending idle slicing probe"); + PeerConnectionManager.writeItem(ctx, new SliceProbeItem()); + } + } +} diff --git a/app/src/main/java/io/xeres/app/net/peer/pipeline/ItemDecoder.java b/app/src/main/java/io/xeres/app/net/peer/pipeline/ItemDecoder.java new file mode 100644 index 000000000..5e28c384e --- /dev/null +++ b/app/src/main/java/io/xeres/app/net/peer/pipeline/ItemDecoder.java @@ -0,0 +1,96 @@ +/* + * Copyright (c) 2019-2020 by David Gerber - https://zapek.com + * + * This file is part of Xeres. + * + * Xeres is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Xeres is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Xeres. If not, see . + */ + +package io.xeres.app.net.peer.pipeline; + +import io.netty.buffer.ByteBuf; +import io.netty.channel.ChannelHandlerContext; +import io.netty.handler.codec.MessageToMessageDecoder; +import io.xeres.app.net.peer.packet.MultiPacket; +import io.xeres.app.net.peer.packet.Packet; +import io.xeres.app.xrs.item.RawItem; + +import java.net.ProtocolException; +import java.util.*; + +/** + * Decodes RS Packets and produces a RawItem. + */ +public class ItemDecoder extends MessageToMessageDecoder +{ + private static final int MAX_SLICES = 16; // maximum number of slices per packets + private static final int MAX_CONCURRENT_PACKETS = 16; // maximum number of concurrent packets + private final Map> accumulator = new HashMap<>(); + + @Override + protected void decode(ChannelHandlerContext ctx, ByteBuf in, List out) throws ProtocolException + { + var packet = Packet.fromBuffer(in); + + if (packet.isMulti()) + { + decodeNewPacket(ctx, (MultiPacket) packet, out); + } + else + { + out.add(new RawItem(packet)); + } + } + + private void decodeNewPacket(ChannelHandlerContext ctx, MultiPacket packet, List out) throws ProtocolException + { + if (packet.isComplete()) + { + if (accumulator.containsKey(packet.getId())) + { + throw new ProtocolException("Start packet " + packet.getId() + " already received"); + } + out.add(new RawItem(packet)); + } + else if (packet.isStart()) + { + if (accumulator.containsKey(packet.getId())) + { + throw new ProtocolException("Start packet " + packet.getId() + " already received"); + } + if (accumulator.size() > MAX_CONCURRENT_PACKETS) + { + throw new ProtocolException("Too many concurrent packets (" + accumulator.size() + ")"); + } + var list = new ArrayList(); + list.add(packet); + accumulator.put(packet.getId(), list); + } + else if (packet.isMiddle()) + { + var list = Optional.ofNullable(accumulator.get(packet.getId())).orElseThrow(() -> new ProtocolException("Middle packet " + packet.getId() + " received without corresponding start packet")); + if (list.size() > MAX_SLICES) + { + throw new ProtocolException("Packet " + packet.getId() + " has too many slices (" + list.size() + ")"); + } + list.add(packet); + } + else if (packet.isEnd()) + { + var list = Optional.ofNullable(accumulator.remove(packet.getId())).orElseThrow(() -> new ProtocolException("End packet " + packet.getId() + " received without corresponding start packet")); + list.add(packet); + out.add(new RawItem(new MultiPacket(ctx, list))); + } + } +} diff --git a/app/src/main/java/io/xeres/app/net/peer/pipeline/ItemEncoder.java b/app/src/main/java/io/xeres/app/net/peer/pipeline/ItemEncoder.java new file mode 100644 index 000000000..cf3fb3136 --- /dev/null +++ b/app/src/main/java/io/xeres/app/net/peer/pipeline/ItemEncoder.java @@ -0,0 +1,39 @@ +/* + * Copyright (c) 2019-2020 by David Gerber - https://zapek.com + * + * This file is part of Xeres. + * + * Xeres is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Xeres is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Xeres. If not, see . + */ + +package io.xeres.app.net.peer.pipeline; + +import io.netty.channel.ChannelHandler; +import io.netty.channel.ChannelHandlerContext; +import io.netty.handler.codec.MessageToMessageEncoder; +import io.xeres.app.net.peer.packet.Packet; +import io.xeres.app.xrs.item.RawItem; + +import java.util.List; + +@ChannelHandler.Sharable +public class ItemEncoder extends MessageToMessageEncoder +{ + @Override + protected void encode(ChannelHandlerContext ctx, RawItem msg, List out) + { + out.add(Packet.fromItem(msg)); + msg.dispose(); + } +} diff --git a/app/src/main/java/io/xeres/app/net/peer/pipeline/MultiPacketEncoder.java b/app/src/main/java/io/xeres/app/net/peer/pipeline/MultiPacketEncoder.java new file mode 100644 index 000000000..0eacfae7d --- /dev/null +++ b/app/src/main/java/io/xeres/app/net/peer/pipeline/MultiPacketEncoder.java @@ -0,0 +1,182 @@ +/* + * Copyright (c) 2019-2020 by David Gerber - https://zapek.com + * + * This file is part of Xeres. + * + * Xeres is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Xeres is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Xeres. If not, see . + */ + +package io.xeres.app.net.peer.pipeline; + +import io.netty.buffer.ByteBuf; +import io.netty.channel.ChannelHandlerContext; +import io.netty.channel.ChannelOutboundHandlerAdapter; +import io.netty.channel.ChannelPromise; +import io.netty.util.concurrent.ScheduledFuture; +import io.xeres.app.net.peer.PeerAttribute; +import io.xeres.app.net.peer.packet.Packet; + +import java.util.PriorityQueue; + +import static io.xeres.app.net.peer.packet.MultiPacket.MAXIMUM_ID; + +// XXX: this is a mess... rewrite it later when I'll have a better architecture. basically I don't even know if I can do that with netty properly +// something like... intercept the writabilityChanged event... don't pass it up as we can still fill in our queue (then pass it, to not gobble all memory). +// then write once we get the event again.. and so on. I think it has to be a ChannelDuplex subclass too! though I don't see where I can get the events... sigh +// or... just write() then use flush() after either 1/2 or 1/4 of a seconds or if a high priority packet comes (should the high priority one really get in front?) +// if it does have to then it's a bit more complicated +public class MultiPacketEncoder extends ChannelOutboundHandlerAdapter // XXX: must extend ChannelOutboundHandlerAdapter +{ + private static final boolean USE_PACKET_SLICING = false; // XXX: set that to TRUE to get the "proper" logic... + private static final boolean USE_PACKET_GROUPING = false; + private static final boolean USE_QOS = false; + + private final PriorityQueue queue = new PriorityQueue<>(); + private ScheduledFuture flusher; + + private int written; + private final int hiWater = Packet.OPTIMAL_PACKET_SIZE; + private int packetId; + + // XXX: we can override read() too! + + @Override + public void write(ChannelHandlerContext ctx, Object msg, ChannelPromise promise) + { + Packet packet = (Packet) msg; + + if (USE_PACKET_SLICING && Boolean.TRUE.equals(ctx.channel().attr(PeerAttribute.MULTI_PACKET).get())) + { + if (USE_PACKET_GROUPING) + { + if (getPacketSizeWithHeader(packet) + written > hiWater) + { + // Slice and send (note that original RS doesn't do it) + } + } + // XXX: if we add to the queue, we need to send a new promise I think... so it knows it was "written" and can read more + // XXX: well, same problem then! it needs ctx.write(Unpooled.EMPTY_BUFFER, promise) + + enqueue(packet); + if (packet.isRealtimePriority()) + { + flusher.cancel(false); + //writePacket(ctx); + ctx.flush(); + } + else + { + if (false) + { + // XXX: this doesn't work because MessageToByteEncoder does send an empty buffer if we didn't write anything. do we have to subclass ChannelOutboundHandlerAdapter then? this seems complicated... there must be an easier way + // XXX: actually! maybe the "easier" way would be to queue before sending... + + // XXX: otherwise! just issue write() calls here and only flush() with the executor. it's the flush() which executes the send() syscall + // so just fill with write() until we reach 512? and either flush directly or with the executor? + // XXX: !!! there's a FlushConsolidationHandler! Check it! -> well, no. this is not what we want + //ByteBuf slice = out.retainedSlice(); + //flusher = ctx.executor().schedule(() -> writePacket(ctx, slice), 250, TimeUnit.MILLISECONDS); // XXX: will have to tweak the delays... also depends on the packet priority I guess. MAKE SURE THIS RUNS ON THE SAME THREAD as encode()!!! + } + else + { + //writePacket(ctx, out); + } + } + } + else + { + // Send the old way + //ctx.writeAndFlush(packet.getData(), promise); // XXX: is this correct?! maybe not... + } + } + + @Override + public void close(ChannelHandlerContext ctx, ChannelPromise promise) throws Exception + { + flusher.cancel(false); + // XXX: do we have to wait for writeFuture()?! + super.close(ctx, promise); + } + + private void enqueue(Packet packet) + { + int remainingSize = packet.getSize(); + + if (remainingSize >= Packet.OPTIMAL_PACKET_SIZE) + { + int offset = 0; + int sequence = 0; + + while (remainingSize > 0) + { + int copySize = Math.min(remainingSize, Packet.OPTIMAL_PACKET_SIZE); + + //RsPacket slice = new RsPacket(packet.getPriority()); + var newData = new byte[copySize]; + //System.arraycopy(packet.getData(), offset, newData, 0, copySize); + //slice.setData(newData); + //slice.setId(packetId); + //slice.setSequence(sequence); + if (offset == 0) + { + //slice.setStart(true); + } + + sequence++; + offset += copySize; + remainingSize -= copySize; + if (remainingSize == 0) + { + //slice.setEnd(true); + } + //queue.add(slice); + } + incrementId(); + } + else + { + queue.add(packet); // XXX: how do we ensure this won't grow too much? (ie. peer stopped responding...). we probably have to send some "read" event down the line + } + } + + private void writePacket(ChannelHandlerContext ctx, Packet packet, ByteBuf out) + { + int sizeWithHeader = getPacketSizeWithHeader(packet); + + out.ensureWritable(sizeWithHeader); // XXX: IndexOutOfBoundsException -> close the connection if it's the case. maybe it already does it + + out.writeByte(Packet.SLICE_PROTOCOL_VERSION_ID_01); + //out.writeByte(packet.getFlags()); + //out.writeInt(packet.getId()); + out.writeShort(packet.getSize()); + //out.writeBytes(packet.getData()); + + ctx.write(out); + written += sizeWithHeader; + } + + private int getPacketSizeWithHeader(Packet packet) + { + return packet.getSize() + 8; + } + + private void incrementId() + { + packetId++; + if (packetId >= MAXIMUM_ID) + { + packetId = 0; + } + } +} diff --git a/app/src/main/java/io/xeres/app/net/peer/pipeline/PacketDecoder.java b/app/src/main/java/io/xeres/app/net/peer/pipeline/PacketDecoder.java new file mode 100644 index 000000000..2673f77be --- /dev/null +++ b/app/src/main/java/io/xeres/app/net/peer/pipeline/PacketDecoder.java @@ -0,0 +1,72 @@ +/* + * Copyright (c) 2019-2020 by David Gerber - https://zapek.com + * + * This file is part of Xeres. + * + * Xeres is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Xeres is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Xeres. If not, see . + */ + +package io.xeres.app.net.peer.pipeline; + +import io.netty.buffer.ByteBuf; +import io.netty.channel.ChannelHandlerContext; +import io.netty.handler.codec.ByteToMessageDecoder; +import io.netty.handler.codec.TooLongFrameException; +import io.xeres.app.net.peer.packet.MultiPacket; +import io.xeres.app.net.peer.packet.Packet; +import io.xeres.app.net.peer.packet.SimplePacket; + +import java.net.ProtocolException; +import java.util.List; + +import static io.xeres.app.net.peer.packet.MultiPacket.MAXIMUM_PACKET_SIZE; +import static io.xeres.app.net.peer.packet.Packet.HEADER_SIZE; + +/** + * Decodes incoming frames into packets. + */ +public class PacketDecoder extends ByteToMessageDecoder +{ + @Override + protected void decode(ChannelHandlerContext ctx, ByteBuf in, List out) throws Exception + { + if (in.readableBytes() >= HEADER_SIZE) + { + long size; + + if (in.getUnsignedByte(in.readerIndex()) == Packet.SLICE_PROTOCOL_VERSION_ID_01) + { + size = (long) in.getUnsignedShort(in.readerIndex() + MultiPacket.HEADER_SIZE_INDEX) + HEADER_SIZE; + } + else + { + size = in.getUnsignedInt(in.readerIndex() + SimplePacket.HEADER_SIZE_INDEX); + } + + if (size >= MAXIMUM_PACKET_SIZE - HEADER_SIZE) + { + throw new TooLongFrameException("Frame is too long: " + size); + } + else if (size < HEADER_SIZE) + { + throw new ProtocolException("Packet size too small, size: " + size); + } + + if (in.readableBytes() >= size) + { + out.add(in.readBytes((int) size)); + } + } + } +} diff --git a/app/src/main/java/io/xeres/app/net/peer/pipeline/PeerHandler.java b/app/src/main/java/io/xeres/app/net/peer/pipeline/PeerHandler.java new file mode 100644 index 000000000..6f49e40ed --- /dev/null +++ b/app/src/main/java/io/xeres/app/net/peer/pipeline/PeerHandler.java @@ -0,0 +1,204 @@ +/* + * Copyright (c) 2019-2021 by David Gerber - https://zapek.com + * + * This file is part of Xeres. + * + * Xeres is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Xeres is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Xeres. If not, see . + */ + +package io.xeres.app.net.peer.pipeline; + +import io.netty.channel.ChannelDuplexHandler; +import io.netty.channel.ChannelHandlerContext; +import io.netty.handler.codec.TooLongFrameException; +import io.netty.handler.ssl.SslHandler; +import io.netty.handler.ssl.SslHandshakeCompletionEvent; +import io.netty.util.ReferenceCountUtil; +import io.xeres.app.database.DatabaseSession; +import io.xeres.app.database.DatabaseSessionManager; +import io.xeres.app.database.model.location.Location; +import io.xeres.app.net.peer.ConnectionDirection; +import io.xeres.app.net.peer.PeerAttribute; +import io.xeres.app.net.peer.PeerConnection; +import io.xeres.app.net.peer.PeerConnectionManager; +import io.xeres.app.net.peer.ssl.SSL; +import io.xeres.app.service.LocationService; +import io.xeres.app.xrs.item.Item; +import io.xeres.app.xrs.item.RawItem; +import io.xeres.app.xrs.service.RsService; +import io.xeres.app.xrs.service.RsServiceType; +import io.xeres.app.xrs.service.serviceinfo.ServiceInfoService; +import io.xeres.app.xrs.service.sliceprobe.item.SliceProbeItem; +import io.xeres.ui.support.tray.TrayService; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.transaction.annotation.Transactional; + +import javax.net.ssl.SSLPeerUnverifiedException; +import java.net.ProtocolException; +import java.security.cert.CertificateException; +import java.util.Locale; +import java.util.concurrent.ThreadLocalRandom; +import java.util.concurrent.TimeUnit; + +import static io.xeres.app.net.peer.ConnectionDirection.INCOMING; + +public class PeerHandler extends ChannelDuplexHandler +{ + private static final Logger log = LoggerFactory.getLogger(PeerHandler.class); + + private final ConnectionDirection direction; + private final LocationService locationService; + private final PeerConnectionManager peerConnectionManager; + private final DatabaseSessionManager databaseSessionManager; + private final ServiceInfoService serviceInfoService; + + public PeerHandler(LocationService locationService, PeerConnectionManager peerConnectionManager, DatabaseSessionManager databaseSessionManager, ServiceInfoService serviceInfoService, ConnectionDirection direction) + { + super(); + this.serviceInfoService = serviceInfoService; + this.direction = direction; + this.peerConnectionManager = peerConnectionManager; + this.databaseSessionManager = databaseSessionManager; + this.locationService = locationService; + } + + @Override + public void channelRead(ChannelHandlerContext ctx, Object msg) + { + var peerConnection = ctx.channel().attr(PeerAttribute.PEER_CONNECTION).get(); + + // Drop messages if SSL peer is not validated + if (peerConnection == null) + { + log.debug("Dropping message as SSL not validated"); + ReferenceCountUtil.release(msg); + return; + } + + log.trace("Got message: {}", msg); + var rawItem = (RawItem) msg; + Item item = null; + var sessionBound = false; + + try + { + item = rawItem.deserialize(); + + RsService service = item.getService(); + if (service != null) + { + var handleItemMethod = service.getClass().getDeclaredMethod("handleItem", PeerConnection.class, Item.class); + if (handleItemMethod.isAnnotationPresent(Transactional.class)) + { + sessionBound = databaseSessionManager.bindSession(); + } + service.handleItem(peerConnection, item); + } + else + { + log.warn("Unknown item. Ignoring."); + } + // XXX: add code to handle non service items (if those exist at all) + } + catch (IllegalArgumentException | NoSuchMethodException e) + { + log.error("Failed to deserialize: {}, {}", e.getClass().getSimpleName(), e.getMessage(), e); + rawItem.dispose(); + } + finally + { + if (sessionBound) + { + databaseSessionManager.unbindSession(); + } + + if (item != null) + { + item.dispose(); // Dispose the item since it was unserialized + } + } + } + + @Override + public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) + { + if (cause instanceof TooLongFrameException || cause instanceof ProtocolException) + { + log.error("Protocol error: {}, {}, closing connection...", cause.getClass(), cause.getMessage()); + log.trace("Stacktrace: ", cause); + ctx.close(); + } + else + { + log.error("Exception in channel:", cause); + } + } + + @Override + public void channelActive(ChannelHandlerContext ctx) + { + log.info("{} connection with {}", direction == INCOMING ? "Incoming" : "Outgoing", ctx.channel().remoteAddress()); + ctx.channel().attr(PeerAttribute.MULTI_PACKET).set(false); + } + + @Override + public void userEventTriggered(ChannelHandlerContext ctx, Object evt) + { + if (evt instanceof SslHandshakeCompletionEvent) + { + try (var session = new DatabaseSession(databaseSessionManager)) + { + Location location; + + synchronized (PeerHandler.class) // Make sure we cannot have an outgoing and incoming connection with the same peer at the same time + { + location = SSL.checkPeerCertificate(locationService, ctx.pipeline().get(SslHandler.class).engine().getSession().getPeerCertificates()); + var peerConnection = peerConnectionManager.addPeer(location, ctx); + locationService.setConnected(location, ctx.channel().remoteAddress()); + ctx.executor().schedule(() -> serviceInfoService.init(peerConnection), ThreadLocalRandom.current().nextInt(2, 9), TimeUnit.SECONDS); + } + + TrayService.showNotification("Established " + direction.toString().toLowerCase(Locale.ROOT) + " connection with " + location.getProfile().getName() + " (" + location.getName() + ")"); + + sendSliceProbe(ctx); + } + catch (CertificateException | SSLPeerUnverifiedException e) + { + log.error("Certificate error: {}", e.getMessage()); + ctx.close(); + } + } + } + + @Override + public void channelInactive(ChannelHandlerContext ctx) + { + log.debug("Closing connection with {}", ctx.channel().remoteAddress()); + var peerConnection = ctx.channel().attr(PeerAttribute.PEER_CONNECTION).get(); + if (peerConnection != null) + { + peerConnection.cleanup(); + locationService.setDisconnected(peerConnection.getLocation()); + peerConnectionManager.removePeer(peerConnection.getLocation()); + } + } + + private void sendSliceProbe(ChannelHandlerContext ctx) + { + var sliceProbeItem = new SliceProbeItem(); + sliceProbeItem.setOutgoing(ctx.alloc(), 2, RsServiceType.PACKET_SLICING_PROBE, 0xCC); + PeerConnectionManager.writeItem(ctx, sliceProbeItem); // this makes the remote RS send packets in the new format + } +} diff --git a/app/src/main/java/io/xeres/app/net/peer/pipeline/SimplePacketEncoder.java b/app/src/main/java/io/xeres/app/net/peer/pipeline/SimplePacketEncoder.java new file mode 100644 index 000000000..5f41c4924 --- /dev/null +++ b/app/src/main/java/io/xeres/app/net/peer/pipeline/SimplePacketEncoder.java @@ -0,0 +1,36 @@ +/* + * Copyright (c) 2019-2020 by David Gerber - https://zapek.com + * + * This file is part of Xeres. + * + * Xeres is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Xeres is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Xeres. If not, see . + */ + +package io.xeres.app.net.peer.pipeline; + +import io.netty.buffer.ByteBuf; +import io.netty.channel.ChannelHandler; +import io.netty.channel.ChannelHandlerContext; +import io.netty.handler.codec.MessageToByteEncoder; +import io.xeres.app.net.peer.packet.Packet; + +@ChannelHandler.Sharable +public class SimplePacketEncoder extends MessageToByteEncoder +{ + @Override + protected void encode(ChannelHandlerContext ctx, Packet msg, ByteBuf out) + { + ctx.writeAndFlush(msg.getBuffer()); // nothing to do, just send the buffer + } +} diff --git a/app/src/main/java/io/xeres/app/net/peer/pipeline/package-info.java b/app/src/main/java/io/xeres/app/net/peer/pipeline/package-info.java new file mode 100644 index 000000000..003fdef32 --- /dev/null +++ b/app/src/main/java/io/xeres/app/net/peer/pipeline/package-info.java @@ -0,0 +1,29 @@ +/* + * Copyright (c) 2019-2020 by David Gerber - https://zapek.com + * + * This file is part of Xeres. + * + * Xeres is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Xeres is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Xeres. If not, see . + */ + +/** + * The pipeline process works in the following way. + *

For incoming packets + *

incoming bytes -> Packet -> Item -> deserialization -> service data
+ *

For outoing packets + *

service data -> serialization -> Item -> Packet -> outgoing bytes
+ *

Right now, the packet encoder sends simple packets. It'll be upgraded to send multi packets later. + * Both multi packets and simple packets are accepted as input. + */ +package io.xeres.app.net.peer.pipeline; diff --git a/app/src/main/java/io/xeres/app/net/peer/ssl/SSL.java b/app/src/main/java/io/xeres/app/net/peer/ssl/SSL.java new file mode 100644 index 000000000..43288a123 --- /dev/null +++ b/app/src/main/java/io/xeres/app/net/peer/ssl/SSL.java @@ -0,0 +1,146 @@ +/* + * Copyright (c) 2019-2021 by David Gerber - https://zapek.com + * + * This file is part of Xeres. + * + * Xeres is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Xeres is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Xeres. If not, see . + */ + +package io.xeres.app.net.peer.ssl; + +import io.netty.handler.ssl.ClientAuth; +import io.netty.handler.ssl.SslContext; +import io.netty.handler.ssl.SslContextBuilder; +import io.netty.handler.ssl.SslProvider; +import io.netty.handler.ssl.util.InsecureTrustManagerFactory; +import io.xeres.app.crypto.pgp.PGP; +import io.xeres.app.crypto.rsa.RSA; +import io.xeres.app.crypto.rsid.RSSerialVersion; +import io.xeres.app.crypto.x509.X509; +import io.xeres.app.database.model.location.Location; +import io.xeres.app.net.peer.ConnectionDirection; +import io.xeres.app.service.LocationService; +import org.bouncycastle.crypto.Digest; +import org.bouncycastle.crypto.digests.SHA1Digest; +import org.bouncycastle.openpgp.PGPException; +import org.bouncycastle.openpgp.PGPPublicKey; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import javax.net.ssl.SSLException; +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.security.InvalidKeyException; +import java.security.NoSuchAlgorithmException; +import java.security.SignatureException; +import java.security.cert.Certificate; +import java.security.cert.CertificateEncodingException; +import java.security.cert.CertificateException; +import java.security.cert.X509Certificate; +import java.security.spec.InvalidKeySpecException; + +import static io.xeres.app.net.peer.ConnectionDirection.INCOMING; + +public final class SSL +{ + private static final Logger log = LoggerFactory.getLogger(SSL.class); + + private SSL() + { + throw new UnsupportedOperationException("Utility class"); + } + + public static SslContext createSslContext(byte[] privateKeyData, X509Certificate certificate, ConnectionDirection direction) throws InvalidKeySpecException, NoSuchAlgorithmException, SSLException + { + SslContextBuilder builder; + if (direction == INCOMING) + { + builder = SslContextBuilder.forServer(RSA.getPrivateKey(privateKeyData), certificate); + } + else + { + builder = SslContextBuilder.forClient() + .keyManager(RSA.getPrivateKey(privateKeyData), certificate); + } + return builder + .sslProvider(SslProvider.OPENSSL_REFCNT) + .protocols("TLSv1.3") + .clientAuth(ClientAuth.REQUIRE) + .trustManager(InsecureTrustManagerFactory.INSTANCE) + .build(); + } + + public static Location checkPeerCertificate(LocationService locationService, Certificate[] chain) throws CertificateException + { + if (chain == null || chain.length == 0) + { + throw new CertificateException("Empty certificate"); + } + + var x509Certificate = X509.getCertificate(chain[0].getEncoded()); + + var locationId = X509.getLocationId(x509Certificate); + log.debug("SSL ID: {}", locationId); + + var location = locationService.findLocationById(locationId).orElseThrow(() -> new CertificateException("Unknown location (SSL ID: " + locationId + ")")); // XXX: don't throw but handle location not found, see below + // XXX: if the location is not found, we can check if we have a profile (accepted=true) that would verify it (verify() method below), then it would be a new location. The pgp identifier is in the issuer field (CN=pgp_id). create the location above instead of the orElseThrow() add orElseGet() + // XXX: what we don't know is the location name so use something like [Unknown] (we need to allow null location names!) and get it with discovery + if (location.isConnected()) + { + throw new CertificateException("Already connected"); + } + log.debug("Found location: {}, already connected: {}", location.getName(), location.isConnected()); + + // XXX: make sure everything is allright and there's no way to fool the system with shortInvites + if (location.getProfile().isComplete()) + { + try + { + verify(PGP.getPGPPublicKey(location.getProfile().getPgpPublicKeyData()), x509Certificate); + } + catch (InvalidKeyException e) + { + throw new CertificateException(e.getMessage(), e); + } + } + return location; + } + + private static void verify(PGPPublicKey pgpPublicKey, X509Certificate cert) throws CertificateException + { + var version = RSSerialVersion.getFromSerialNumber(cert.getSerialNumber()); + log.debug("Certificate version: {}", version); + + try + { + byte[] in = cert.getTBSCertificate(); + + if (version.ordinal() < RSSerialVersion.V07_0001.ordinal()) + { + // If this is a 0.6 certificate, the signature verification is performed + // on the hash of the certificate + var hash = new byte[20]; + Digest digest = new SHA1Digest(); + digest.update(in, 0, in.length); + digest.doFinal(hash, 0); + in = hash; + } + PGP.verify(pgpPublicKey, cert.getSignature(), new ByteArrayInputStream(in)); + } + catch (CertificateEncodingException | IOException | SignatureException | PGPException e) + { + throw new CertificateException(e); + } + } +} diff --git a/app/src/main/java/io/xeres/app/net/protocol/PeerAddress.java b/app/src/main/java/io/xeres/app/net/protocol/PeerAddress.java new file mode 100644 index 000000000..388cc3910 --- /dev/null +++ b/app/src/main/java/io/xeres/app/net/protocol/PeerAddress.java @@ -0,0 +1,441 @@ +/* + * Copyright (c) 2019-2021 by David Gerber - https://zapek.com + * + * This file is part of Xeres. + * + * Xeres is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Xeres is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Xeres. If not, see . + */ + +package io.xeres.app.net.protocol; + +import io.xeres.app.net.protocol.tor.OnionAddress; +import io.xeres.common.protocol.ip.IP; + +import java.net.Inet4Address; +import java.net.InetSocketAddress; +import java.net.SocketAddress; +import java.net.UnknownHostException; +import java.util.Arrays; +import java.util.Optional; + +import static io.xeres.app.net.protocol.PeerAddress.Type.*; +import static java.util.function.Predicate.not; + +/** + * A class that can contain any peer address. + *

+ * Vocabulary: + *

    + *
  • url: a Retroshare URL (ipv4://192.168.1.1:80, etc...)
  • + *
  • address: a string that can be an ipv4 socket or tor address (192.168.1.1:80, foobar.onion, ...)
  • + *
  • ipAndPort: 192.168.1.1:80
  • + *
  • socket address: an ip socket address, directly usable with java functions
  • + *
+ *

Creating a PeerAddress always succeed. Its validity can be checked with isValid(). + */ +public final class PeerAddress +{ + private static final String PREFIX_IPV4 = "ipv4://"; + private static final String PREFIX_IPV6 = "ipv6://"; + + public enum Type + { + INVALID, + IPV4, + IPV6, + TOR, + HOSTNAME, + I2P + } + + private SocketAddress socketAddress; + private final Type type; + + /** + * Creates a PeerAddress from an URL (ipv4://, etc...). + * + * @param url the URL + * @return a PeerAddress + */ + public static PeerAddress fromUrl(String url) + { + if (url == null) + { + return fromInvalid(); + } + + if (url.startsWith(PREFIX_IPV4)) + { + return fromIpAndPort(url.substring(PREFIX_IPV4.length())); + } + return fromInvalid(); + } + + /** + * Creates a PeerAddress from an address (eg. juiejkslajfsk.onion, 85.12.33.11:8081, ...). + * + * @param address the address + * @return a PeerAddress + */ + public static PeerAddress fromAddress(String address) + { + if (address == null) + { + return fromInvalid(); + } + return tryFromHidden(address).orElse(tryFromIpAndPort(address).orElse(fromHostnameAndPort(address))); + } + + /** + * Creates a PeerAddress from a hidden address (Tor) + * + * @param address the address + * @return a PeerAddress + */ + public static PeerAddress fromHidden(String address) + { + if (address == null) + { + return fromInvalid(); + } + return tryFromHidden(address).orElse(fromInvalid()); + } + + private static Optional tryFromHidden(String address) + { + if (address.endsWith(".onion")) + { + return tryFromOnion(address); + } + return Optional.empty(); + } + + private static Optional tryFromIpAndPort(String address) + { + var peerAddress = fromIpAndPort(address); + if (peerAddress.isValid()) + { + return Optional.of(peerAddress); + } + return Optional.empty(); + } + + /** + * Creates a PeerAddress from an IP and a port. + * + * @param ip the IP address + * @param port the port + * @return a PeerAddress + */ + public static PeerAddress from(String ip, int port) + { + if (isInvalidPort(port)) + { + return fromInvalid(); + } + if (isInvalidIpAddress(ip)) + { + return fromInvalid(); + } + try + { + return new PeerAddress(new InetSocketAddress(Inet4Address.getByName(ip), port), IPV4); + } + catch (UnknownHostException e) + { + return fromInvalid(); // Won't happen anyway + } + } + + /** + * Creates a PeerAddress from an "ip:port" string. + * + * @param ipAndPort a string in the form "ip:port", eg. "192.168.1.2:8002" + * @return a PeerAddress + */ + public static PeerAddress fromIpAndPort(String ipAndPort) + { + int port; + String[] tokens = ipAndPort.split(":"); + + if (tokens.length != 2) + { + return fromInvalid(); + } + + try + { + port = Integer.parseInt(tokens[1]); + } + catch (NumberFormatException e) + { + return fromInvalid(); + } + return from(tokens[0], port); + } + + /** + * Creates a PeerAddress from a RsCertificate byte array. + * + * @param data a byte array which is made of the 4 bytes of the IP and the 2 bytes of the port (big endian). + * @return a PeerAddress + */ + public static PeerAddress fromByteArray(byte[] data) + { + if (data == null || data.length != 6) + { + return fromInvalid(); + } + + var ip = String.format("%d.%d.%d.%d", Byte.toUnsignedInt(data[0]), Byte.toUnsignedInt(data[1]), Byte.toUnsignedInt(data[2]), Byte.toUnsignedInt(data[3])); + + if (isInvalidIpAddress(ip)) + { + return fromInvalid(); + } + + int port = Byte.toUnsignedInt(data[4]) << 8 | Byte.toUnsignedInt(data[5]); + if (isInvalidPort(port)) + { + return fromInvalid(); + } + return from(ip, port); + } + + public static PeerAddress fromHostname(String hostname, int port) + { + if (isInvalidHostname(hostname) || isInvalidPort(port)) + { + return fromInvalid(); + } + return new PeerAddress(InetSocketAddress.createUnresolved(hostname, port), HOSTNAME); + } + + public static PeerAddress fromHostnameAndPort(String hostnameAndPort) + { + int port; + String[] tokens = hostnameAndPort.split(":"); + + if (tokens.length != 2) + { + return fromInvalid(); + } + + try + { + port = Integer.parseInt(tokens[1]); + } + catch (NumberFormatException e) + { + return fromInvalid(); + } + if (isInvalidHostname(tokens[0])) + { + return fromInvalid(); + } + return fromHostname(tokens[0], port); + } + + public static PeerAddress fromSocketAddress(SocketAddress socketAddress) + { + return new PeerAddress(socketAddress, Type.IPV4); + } + + /** + * Creates a PeerAddress from an onion address (ie. "jskljfksdjk.onion") + * + * @param onion the onion address + * @return a PeerAddress + */ + public static PeerAddress fromOnion(String onion) + { + return tryFromOnion(onion).orElse(fromInvalid()); + } + + private static Optional tryFromOnion(String onion) + { + if (OnionAddress.isValidAddress(onion)) + { + return Optional.of(new PeerAddress(TOR)); + } + return Optional.empty(); + } + + /** + * Creates an invalid PeerAddress. + * + * @return a PeerAddress + */ + public static PeerAddress fromInvalid() + { + return new PeerAddress(INVALID); + } + + private PeerAddress(Type type) + { + this.type = type; + } + + private PeerAddress(SocketAddress socketAddress, Type type) + { + this.type = type; + this.socketAddress = socketAddress; + } + + /** + * Gets the SocketAddress of the PeerAddress (if the protocol allows it). + * + * @return a SocketAddress + */ + public SocketAddress getSocketAddress() + { + return socketAddress; + } + + /** + * Gets the IP address and port of the PeerAddress (if the protocol allows it). + * + * @return the IP address and port in the following format: "ip:port" + */ + public Optional getAddress() + { + if (socketAddress instanceof InetSocketAddress inetSocketAddress) + { + return Optional.of(inetSocketAddress.getHostString() + ":" + inetSocketAddress.getPort()); + } + return Optional.empty(); + } + + /** + * Gets the IP address and port in an array of bytes. + * + * @return the IP address in the 4 first bytes and the port in the 2 last ones (big endian). + */ + public Optional getAddressAsBytes() + { + if (socketAddress instanceof InetSocketAddress inetSocketAddress) + { + int port = inetSocketAddress.getPort(); + var bytes = new byte[6]; + System.arraycopy(inetSocketAddress.getAddress().getAddress(), 0, bytes, 0, 4); + bytes[4] = (byte) (port >> 8); + bytes[5] = (byte) (port & 0xff); + return Optional.of(bytes); + } + return Optional.empty(); + } + + /** + * Gets the type of the PeerAddress. + * + * @return the type of the PeerAddress + */ + public Type getType() + { + return type; + } + + /** + * Checks if the PeerAddress is invalid. + * + * @return true if invalid + */ + public boolean isInvalid() + { + return type == INVALID; + } + + /** + * Checks if the PeerAddress is valid. + * + * @return true if valid + */ + public boolean isValid() + { + return type != INVALID; + } + + /** + * Checks if the PeerAddress is a hidden address (Tor) + * + * @return true if the address is a hidden address + */ + public boolean isHidden() + { + return type == TOR; + } + + /** + * Checks if the PeerAddress is an external address (ie. something that can be connected to from outside a LAN). + * + * @return true if external address + */ + public boolean isExternal() + { + return type == TOR || + (type == IPV4 && IP.isPublicIp(((InetSocketAddress) socketAddress).getHostString())); + } + + public boolean isLAN() + { + return type == IPV4 && IP.isLanIp(((InetSocketAddress) socketAddress).getHostString()); + } + + public boolean isHostname() + { + return type == HOSTNAME; + } + + private static boolean isInvalidIpAddress(String address) + { + String[] octets = address.split("\\."); + + if (octets.length != 4) + { + return true; + } + + try + { + return Arrays.stream(octets) + .filter(not(s -> s.length() > 1 && s.startsWith("0"))) + .map(Integer::parseInt) + .filter(i -> (i >= 0 && i <= 255)) + .count() != 4 || !IP.isRoutableIp(address); + } + catch (NumberFormatException e) + { + return true; + } + } + + private static boolean isInvalidPort(int port) + { + return !IP.isValidPort(port); + } + + private static boolean isInvalidHostname(String hostname) + { + return hostname != null && hostname.length() <= 253; + } + + @Override + public String toString() + { + return "PeerAddress{" + + "socketAddress=" + socketAddress + + ", type=" + type + + '}'; + } +} diff --git a/app/src/main/java/io/xeres/app/net/protocol/tor/OnionAddress.java b/app/src/main/java/io/xeres/app/net/protocol/tor/OnionAddress.java new file mode 100644 index 000000000..94fc70133 --- /dev/null +++ b/app/src/main/java/io/xeres/app/net/protocol/tor/OnionAddress.java @@ -0,0 +1,37 @@ +/* + * Copyright (c) 2019-2020 by David Gerber - https://zapek.com + * + * This file is part of Xeres. + * + * Xeres is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Xeres is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Xeres. If not, see . + */ + +package io.xeres.app.net.protocol.tor; + +import java.util.regex.Pattern; + +public final class OnionAddress +{ + private static final Pattern ONION_PATTERN = Pattern.compile("[a-z2-7]{56}\\.onion"); + + private OnionAddress() + { + throw new UnsupportedOperationException("Utility class"); + } + + public static boolean isValidAddress(String host) + { + return ONION_PATTERN.matcher(host).matches(); + } +} diff --git a/app/src/main/java/io/xeres/app/net/protocol/tor/package-info.java b/app/src/main/java/io/xeres/app/net/protocol/tor/package-info.java new file mode 100644 index 000000000..e6931c4da --- /dev/null +++ b/app/src/main/java/io/xeres/app/net/protocol/tor/package-info.java @@ -0,0 +1,23 @@ +/* + * Copyright (c) 2019-2020 by David Gerber - https://zapek.com + * + * This file is part of Xeres. + * + * Xeres is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Xeres is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Xeres. If not, see . + */ + +/** + * Tor protocol support. + */ +package io.xeres.app.net.protocol.tor; diff --git a/app/src/main/java/io/xeres/app/net/tou/package-info.java b/app/src/main/java/io/xeres/app/net/tou/package-info.java new file mode 100644 index 000000000..01377135e --- /dev/null +++ b/app/src/main/java/io/xeres/app/net/tou/package-info.java @@ -0,0 +1,24 @@ +/* + * Copyright (c) 2019-2020 by David Gerber - https://zapek.com + * + * This file is part of Xeres. + * + * Xeres is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Xeres is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Xeres. If not, see . + */ + +/** + * TCP over UDP. Will be implemented later on because it's complex and not strictly necessary. + * Also a pure UDP protocol (DTLS) might be better for some things like realtime audio/video. + */ +package io.xeres.app.net.tou; diff --git a/app/src/main/java/io/xeres/app/net/upnp/ControlPoint.java b/app/src/main/java/io/xeres/app/net/upnp/ControlPoint.java new file mode 100644 index 000000000..8fd39c3c5 --- /dev/null +++ b/app/src/main/java/io/xeres/app/net/upnp/ControlPoint.java @@ -0,0 +1,220 @@ +/* + * Copyright (c) 2019-2021 by David Gerber - https://zapek.com + * + * This file is part of Xeres. + * + * Xeres is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Xeres is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Xeres. If not, see . + */ + +package io.xeres.app.net.upnp; + +import io.xeres.common.AppName; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.w3c.dom.Node; +import org.xml.sax.SAXException; + +import javax.xml.XMLConstants; +import javax.xml.parsers.DocumentBuilderFactory; +import javax.xml.parsers.ParserConfigurationException; +import javax.xml.xpath.XPathException; +import javax.xml.xpath.XPathExpressionException; +import javax.xml.xpath.XPathFactory; +import javax.xml.xpath.XPathNodes; +import java.io.ByteArrayInputStream; +import java.io.FileNotFoundException; +import java.io.IOException; +import java.net.URL; +import java.util.HashMap; +import java.util.Locale; +import java.util.Map; + +final class ControlPoint +{ + private static final Logger log = LoggerFactory.getLogger(ControlPoint.class); + + private ControlPoint() + { + throw new UnsupportedOperationException("Utility class"); + } + + static boolean updateDevice(DeviceSpecs upnpDevice, URL location) + { + var controlUrlFound = false; + + try + { + var document = getDocumentBuilderFactory().newDocumentBuilder().parse(location.toString()); + var xPath = XPathFactory.newInstance().newXPath(); + + XPathNodes devices = xPath.evaluateExpression("//device[deviceType[contains(text(), 'InternetGatewayDevice')]]", document, XPathNodes.class); + + getDeviceInfo(upnpDevice, devices); + + XPathNodes services = xPath.evaluateExpression("//service[serviceType[contains(text(), 'WANIPConnection') or contains(text(), 'WANPPPConnection')]]", document, XPathNodes.class); + + controlUrlFound = getServices(upnpDevice, controlUrlFound, services); + } + catch (FileNotFoundException e) + { + log.error("UPNP router's URL {} is not accessible", location); + } + catch (ParserConfigurationException e) + { + log.error("Couldn't create XML parser for UPNP router URL {}: {}", location, e.getMessage()); + } + catch (SAXException e) + { + log.error("XML parse error for UPNP router URL {}: {}", location, e.getMessage()); + } + catch (XPathException e) + { + throw new IllegalArgumentException("XPath expression error: " + e.getMessage(), e); + } + catch (IOException e) + { + log.error("I/O error when parsing UPNP's router URL {}: {}", location, e.getMessage()); + } + return controlUrlFound; + } + + private static void getDeviceInfo(DeviceSpecs upnpDevice, XPathNodes devices) throws XPathException + { + if (devices.size() != 1) + { + throw new IllegalStateException("Require 1 root device, found: " + devices.size()); + } + + var childNodes = devices.get(0).getChildNodes(); + for (var i = 0; i < childNodes.getLength(); i++) + { + var item = childNodes.item(i); + switch (item.getNodeName().toLowerCase(Locale.ROOT)) + { + case "modelname" -> upnpDevice.setModelName(item.getTextContent().trim()); + case "manufacturer" -> upnpDevice.setManufacturer(item.getTextContent().trim()); + case "manufacturerurl" -> upnpDevice.setManufacturerUrl(item.getTextContent().trim()); + case "serialnumber" -> upnpDevice.setSerialNumber(item.getTextContent().trim()); + case "presentationurl" -> upnpDevice.setPresentationUrl(item.getTextContent().trim()); + default -> log.trace("node: {}", item.getNodeName()); + } + } + } + + private static boolean getServices(DeviceSpecs upnpDevice, boolean controlUrlFound, XPathNodes services) throws XPathException + { + if (services.size() != 1) + { + throw new IllegalStateException("More than one service: " + services.size()); + } + + var childNodes = services.get(0).getChildNodes(); + for (var i = 0; i < childNodes.getLength(); i++) + { + var item = childNodes.item(i); + switch (item.getNodeName().toLowerCase(Locale.ROOT)) + { + case "controlurl" -> { + upnpDevice.setControlUrl(item.getTextContent().trim()); + controlUrlFound = true; + } + case "servicetype" -> upnpDevice.setServiceType(item.getTextContent().trim()); + default -> log.trace("service: {}", item.getNodeName()); + } + } + return controlUrlFound; + } + + static boolean addPortMapping(URL controlUrl, String serviceType, String internalIp, int internalPort, int externalPort, int duration, Protocol protocol) + { + Map args = new HashMap<>(); + args.put("NewRemoteHost", ""); + args.put("NewExternalPort", String.valueOf(externalPort)); + args.put("NewProtocol", protocol.name()); + args.put("NewInternalPort", String.valueOf(internalPort)); + args.put("NewInternalClient", internalIp); + args.put("NewEnabled", "1"); + args.put("NewPortMappingDescription", AppName.NAME + " " + protocol.name()); + args.put("NewLeaseDuration", String.valueOf(duration)); + + ResponseEntity response = Soap.sendRequest(controlUrl, serviceType, "AddPortMapping", args); + return response.getStatusCode() == HttpStatus.OK; + } + + static boolean removePortMapping(URL controlUrl, String serviceType, int externalPort, Protocol protocol) + { + Map args = new HashMap<>(); + args.put("NewRemoteHost", ""); + args.put("NewExternalPort", String.valueOf(externalPort)); + args.put("NewProtocol", protocol.name()); + + ResponseEntity response = Soap.sendRequest(controlUrl, serviceType, "DeletePortMapping", args); + return response.getStatusCode() == HttpStatus.OK; + } + + static String getExternalIpAddress(URL controlUrl, String serviceType) + { + ResponseEntity response = Soap.sendRequest(controlUrl, serviceType, "GetExternalIPAddress", null); + var body = response.getBody(); + if (response.getStatusCode() == HttpStatus.OK && body != null) + { + Map reply = getTextNodes(body); + return reply.getOrDefault("NewExternalIPAddress", ""); + } + return ""; + } + + static Map getTextNodes(String xml) + { + Map result = new HashMap<>(); + try + { + var document = getDocumentBuilderFactory().newDocumentBuilder().parse(new ByteArrayInputStream(xml.getBytes())); + var xPath = XPathFactory.newInstance().newXPath(); + var textNodes = xPath.evaluateExpression("//text()", document, XPathNodes.class); + + for (Node textNode : textNodes) + { + result.put(textNode.getParentNode().getNodeName(), textNode.getTextContent()); + } + } + catch (SAXException e) + { + throw new IllegalArgumentException("XML parse error on UPNP router reply: " + e.getMessage(), e); + } + catch (IOException e) + { + throw new IllegalArgumentException("I/O error when parsing UPNP router's XML reply: " + e.getMessage(), e); + } + catch (ParserConfigurationException e) + { + throw new IllegalArgumentException("Couldn't create XML parser for UPNP router's XML reply: " + e.getMessage(), e); + } + catch (XPathExpressionException e) + { + throw new IllegalArgumentException("XPath expression error: " + e.getMessage(), e); + } + return result; + } + + private static DocumentBuilderFactory getDocumentBuilderFactory() + { + var df = DocumentBuilderFactory.newInstance(); + df.setAttribute(XMLConstants.ACCESS_EXTERNAL_DTD, ""); + df.setAttribute(XMLConstants.ACCESS_EXTERNAL_SCHEMA, ""); + return df; + } +} diff --git a/app/src/main/java/io/xeres/app/net/upnp/Device.java b/app/src/main/java/io/xeres/app/net/upnp/Device.java new file mode 100644 index 000000000..13f9eb2b9 --- /dev/null +++ b/app/src/main/java/io/xeres/app/net/upnp/Device.java @@ -0,0 +1,387 @@ +/* + * Copyright (c) 2019-2021 by David Gerber - https://zapek.com + * + * This file is part of Xeres. + * + * Xeres is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Xeres is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Xeres. If not, see . + */ + +package io.xeres.app.net.upnp; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.BufferedReader; +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.io.InputStreamReader; +import java.net.InetSocketAddress; +import java.net.MalformedURLException; +import java.net.SocketAddress; +import java.net.URL; +import java.nio.ByteBuffer; +import java.nio.charset.StandardCharsets; +import java.util.*; +import java.util.regex.Pattern; + +final class Device implements DeviceSpecs +{ + private static final Logger log = LoggerFactory.getLogger(Device.class); + + private static final Pattern HTTP_OK_PATTERN = Pattern.compile("^HTTP/1\\.. 200 OK"); + private static final int MAX_HEADER_VALUE_LENGTH = 128; + + private static final Set supportedHeaders = EnumSet.allOf(HttpuHeader.class); + + private InetSocketAddress inetSocketAddress; + private Map headers; + + private String modelName; + private String manufacturer; + private URL manufacturerUrl; + private String serialNumber; + private URL presentationUrl; + private URL controlUrl; + private URL locationUrl; + private String serviceType; + private boolean hasControlPoint; + private final HashSet ports = new HashSet<>(); + + static Device from(SocketAddress socketAddress, ByteBuffer byteBuffer) + { + if (!(socketAddress instanceof InetSocketAddress)) + { + log.warn("Not an Inet device. Ignoring."); + return Device.fromInvalid(); + } + + var reader = new BufferedReader(new InputStreamReader(new ByteArrayInputStream(byteBuffer.array()), StandardCharsets.US_ASCII)); + + try + { + Map headers = new EnumMap<>(HttpuHeader.class); + String s = reader.readLine(); + + if (!HTTP_OK_PATTERN.matcher(s).matches()) + { + log.warn("Not a valid HTTP response: {}. Ignoring.", s); + return Device.fromInvalid(); + } + + while ((s = reader.readLine()) != null) + { + String[] tokens = s.split(":", 2); + if (tokens.length != 2 || tokens[1].length() > MAX_HEADER_VALUE_LENGTH) + { + continue; + } + + String header = tokens[0].toUpperCase(Locale.ROOT).strip(); + if (supportedHeaders.stream().anyMatch(h -> h.name().equals(header))) + { + headers.put(HttpuHeader.valueOf(header), tokens[1].strip()); + } + } + return new Device(socketAddress, headers); + } + catch (IOException e) + { + log.warn("Couldn't read line, shouldn't happen", e); + } + return Device.fromInvalid(); + } + + private static Device fromInvalid() + { + return new Device(); + } + + private Device(SocketAddress socketAddress, Map headers) + { + this.inetSocketAddress = (InetSocketAddress) socketAddress; + this.headers = headers; + } + + private Device() + { + + } + + public boolean isValid() + { + return inetSocketAddress != null && hasLocation(); + } + + public boolean isInvalid() + { + return inetSocketAddress == null; + } + + public InetSocketAddress getInetSocketAddress() + { + return inetSocketAddress; + } + + public Optional getHeaderValue(HttpuHeader header) + { + if (isInvalid()) + { + return Optional.empty(); + } + return Optional.ofNullable(headers.get(header)); + } + + public boolean hasLocation() + { + return getLocationUrl() != null; + } + + public URL getLocationUrl() + { + if (locationUrl != null) + { + return locationUrl; + } + + locationUrl = getHeaderValue(HttpuHeader.LOCATION).map(s -> { + try + { + return new URL(s); + } + catch (MalformedURLException e) + { + log.error("UPNP: unparseable URL {}, {}", s, e.getMessage()); + return null; + } + }).orElse(null); + return locationUrl; + } + + public boolean hasServer() + { + return getHeaderValue(HttpuHeader.SERVER).isPresent(); + } + + public String getServer() + { + return getHeaderValue(HttpuHeader.SERVER).orElse(null); + } + + public boolean hasUsn() + { + return getHeaderValue(HttpuHeader.USN).isPresent(); + } + + public String getUsn() + { + return getHeaderValue(HttpuHeader.USN).orElse(null); + } + + @Override + public boolean hasModelName() + { + return modelName != null; + } + + @Override + public String getModelName() + { + return modelName; + } + + @Override + public void setModelName(String modelName) + { + this.modelName = modelName; + } + + @Override + public boolean hasManufacturer() + { + return manufacturer != null; + } + + @Override + public String getManufacturer() + { + return manufacturer; + } + + @Override + public void setManufacturer(String manufacturer) + { + this.manufacturer = manufacturer; + } + + @Override + public URL getManufacturerUrl() + { + return manufacturerUrl; + } + + @Override + public void setManufacturerUrl(String manufacturerUrl) + { + this.manufacturerUrl = parseUrl(manufacturerUrl); + } + + @Override + public boolean hasSerialNumber() + { + return serialNumber != null; + } + + @Override + public String getSerialNumber() + { + return serialNumber; + } + + @Override + public void setSerialNumber(String serialNumber) + { + this.serialNumber = serialNumber; + } + + @Override + public boolean hasControlUrl() + { + return controlUrl != null; + } + + @Override + public URL getControlUrl() + { + return controlUrl; + } + + @Override + public void setControlUrl(String controlUrl) + { + this.controlUrl = parseUrl(locationUrl, controlUrl); + } + + @Override + public boolean hasPresentationUrl() + { + return presentationUrl != null; + } + + @Override + public URL getPresentationUrl() + { + return presentationUrl; + } + + @Override + public void setPresentationUrl(String presentationUrl) + { + this.presentationUrl = parseUrl(presentationUrl); + } + + @Override + public String getServiceType() + { + return serviceType; + } + + @Override + public void setServiceType(String serviceType) + { + this.serviceType = serviceType; + } + + public void addControlPoint() + { + if (isInvalid()) + { + throw new IllegalStateException("Trying to add a control point to an invalid device"); + } + hasControlPoint = ControlPoint.updateDevice(this, getLocationUrl()); + } + + public boolean hasControlPoint() + { + return hasControlPoint; + } + + public boolean addPortMapping(String internalIp, int internalPort, int externalPort, int duration, Protocol protocol) + { + boolean added = ControlPoint.addPortMapping(getControlUrl(), getServiceType(), internalIp, internalPort, externalPort, duration, protocol); + if (added) + { + ports.add(new PortMapping(externalPort, protocol)); + } + return added; + } + + public void deletePortMapping(int externalPort, Protocol protocol) + { + if (ControlPoint.removePortMapping(getControlUrl(), getServiceType(), externalPort, protocol)) + { + ports.removeIf(portMapping -> portMapping.getPort() == externalPort && portMapping.getProtocol() == protocol); + } + } + + public void removeAllPortMapping() + { + new HashSet<>(this.ports).forEach(portMapping -> deletePortMapping(portMapping.getPort(), portMapping.getProtocol())); + } + + public String getExternalIpAddress() + { + return ControlPoint.getExternalIpAddress(getControlUrl(), getServiceType()); + } + + private URL parseUrl(String url) + { + return parseUrl(null, url); + } + + private URL parseUrl(URL baseUrl, String url) + { + try + { + if (baseUrl != null) + { + return new URL(baseUrl, url); + } + return new URL(url); + } + catch (MalformedURLException e) + { + log.error("Wrong URL {}, {}", url, e.getMessage()); + return null; + } + } + + @Override + public String toString() + { + return "Device{" + + "inetSocketAddress=" + inetSocketAddress + + ", headers=" + headers + + ", modelName='" + modelName + '\'' + + ", manufacturer='" + manufacturer + '\'' + + ", manufacturerUrl=" + manufacturerUrl + + ", serialNumber='" + serialNumber + '\'' + + ", presentationUrl=" + presentationUrl + + ", controlUrl=" + controlUrl + + ", locationUrl=" + locationUrl + + ", serviceType='" + serviceType + '\'' + + ", hasControlPoint=" + hasControlPoint + + ", ports=" + ports + + '}'; + } +} diff --git a/app/src/main/java/io/xeres/app/net/upnp/DeviceSpecs.java b/app/src/main/java/io/xeres/app/net/upnp/DeviceSpecs.java new file mode 100644 index 000000000..0e8385f6b --- /dev/null +++ b/app/src/main/java/io/xeres/app/net/upnp/DeviceSpecs.java @@ -0,0 +1,63 @@ +/* + * Copyright (c) 2019-2021 by David Gerber - https://zapek.com + * + * This file is part of Xeres. + * + * Xeres is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Xeres is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Xeres. If not, see . + */ + +package io.xeres.app.net.upnp; + +import java.net.URL; + +public interface DeviceSpecs +{ + boolean hasModelName(); + + String getModelName(); + + void setModelName(String modelName); + + boolean hasManufacturer(); + + String getManufacturer(); + + void setManufacturer(String manufacturer); + + URL getManufacturerUrl(); + + void setManufacturerUrl(String manufacturerUrl); + + boolean hasSerialNumber(); + + String getSerialNumber(); + + void setSerialNumber(String serialNumber); + + boolean hasControlUrl(); + + URL getControlUrl(); + + void setControlUrl(String controlUrl); + + boolean hasPresentationUrl(); + + URL getPresentationUrl(); + + void setPresentationUrl(String presentationUrl); + + String getServiceType(); + + void setServiceType(String serviceType); +} diff --git a/app/src/main/java/io/xeres/app/net/upnp/HttpuHeader.java b/app/src/main/java/io/xeres/app/net/upnp/HttpuHeader.java new file mode 100644 index 000000000..428280ae1 --- /dev/null +++ b/app/src/main/java/io/xeres/app/net/upnp/HttpuHeader.java @@ -0,0 +1,28 @@ +/* + * Copyright (c) 2019-2020 by David Gerber - https://zapek.com + * + * This file is part of Xeres. + * + * Xeres is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Xeres is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Xeres. If not, see . + */ + +package io.xeres.app.net.upnp; + +enum HttpuHeader +{ + LOCATION, + SERVER, + ST, + USN +} diff --git a/app/src/main/java/io/xeres/app/net/upnp/PortMapping.java b/app/src/main/java/io/xeres/app/net/upnp/PortMapping.java new file mode 100644 index 000000000..c6d378c73 --- /dev/null +++ b/app/src/main/java/io/xeres/app/net/upnp/PortMapping.java @@ -0,0 +1,60 @@ +/* + * Copyright (c) 2019-2020 by David Gerber - https://zapek.com + * + * This file is part of Xeres. + * + * Xeres is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Xeres is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Xeres. If not, see . + */ + +package io.xeres.app.net.upnp; + +import java.util.Objects; + +class PortMapping +{ + final int port; + final Protocol protocol; + + PortMapping(int port, Protocol protocol) + { + this.port = port; + this.protocol = protocol; + } + + public int getPort() + { + return port; + } + + public Protocol getProtocol() + { + return protocol; + } + + @Override + public boolean equals(Object o) + { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + PortMapping that = (PortMapping) o; + return port == that.port && + protocol == that.protocol; + } + + @Override + public int hashCode() + { + return Objects.hash(port, protocol); + } +} diff --git a/app/src/main/java/io/xeres/app/net/upnp/Protocol.java b/app/src/main/java/io/xeres/app/net/upnp/Protocol.java new file mode 100644 index 000000000..44a8f1547 --- /dev/null +++ b/app/src/main/java/io/xeres/app/net/upnp/Protocol.java @@ -0,0 +1,26 @@ +/* + * Copyright (c) 2019-2021 by David Gerber - https://zapek.com + * + * This file is part of Xeres. + * + * Xeres is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Xeres is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Xeres. If not, see . + */ + +package io.xeres.app.net.upnp; + +enum Protocol +{ + TCP, + UDP +} diff --git a/app/src/main/java/io/xeres/app/net/upnp/Soap.java b/app/src/main/java/io/xeres/app/net/upnp/Soap.java new file mode 100644 index 000000000..a04832336 --- /dev/null +++ b/app/src/main/java/io/xeres/app/net/upnp/Soap.java @@ -0,0 +1,92 @@ +/* + * Copyright (c) 2019-2021 by David Gerber - https://zapek.com + * + * This file is part of Xeres. + * + * Xeres is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Xeres is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Xeres. If not, see . + */ + +package io.xeres.app.net.upnp; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpStatus; +import org.springframework.http.MediaType; +import org.springframework.http.ResponseEntity; +import org.springframework.web.reactive.function.client.WebClient; +import org.springframework.web.reactive.function.client.WebClientException; + +import java.net.URL; +import java.time.Duration; +import java.util.Map; + +final class Soap +{ + private static final Logger log = LoggerFactory.getLogger(Soap.class); + + private Soap() + { + throw new UnsupportedOperationException("Utility class"); + } + + private static String createSoap(String serviceType, String actionName, Map args) + { + var soap = new StringBuilder(); + + soap.append("\r\n"); + soap.append(""); + soap.append(""); + soap.append(""); + + if (args != null) + { + args.forEach((key, value) -> soap.append("<").append(key).append(">").append(value).append("")); + } + + soap.append(""); + soap.append(""); + soap.append(""); + + return soap.toString(); + } + + static ResponseEntity sendRequest(URL controlUrl, String serviceType, String action, Map args) + { + var webClient = WebClient.builder() + .baseUrl(controlUrl.toString()) + .defaultHeader(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_XML_VALUE) + .build(); + + try + { + return webClient.post() + .bodyValue(createSoap(serviceType, action, args)) + .header("SOAPAction", "\"" + serviceType + "#" + action + "\"") + .retrieve() + .toEntity(String.class) + .block(Duration.ofSeconds(10)); + } + catch (WebClientException e) + { + log.error("Bad request: {}", e.getMessage()); + return ResponseEntity.badRequest().build(); + } + catch (RuntimeException e) + { + log.error("Timeout while sending request: {}", e.getMessage()); + return ResponseEntity.status(HttpStatus.REQUEST_TIMEOUT).build(); + } + } +} diff --git a/app/src/main/java/io/xeres/app/net/upnp/UPNPService.java b/app/src/main/java/io/xeres/app/net/upnp/UPNPService.java new file mode 100644 index 000000000..01f0a8ca2 --- /dev/null +++ b/app/src/main/java/io/xeres/app/net/upnp/UPNPService.java @@ -0,0 +1,347 @@ +/* + * Copyright (c) 2019-2021 by David Gerber - https://zapek.com + * + * This file is part of Xeres. + * + * Xeres is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Xeres is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Xeres. If not, see . + */ + +package io.xeres.app.net.upnp; + +import io.xeres.common.AppName; +import io.xeres.common.protocol.ip.IP; +import io.xeres.ui.client.ConfigClient; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.stereotype.Service; +import reactor.core.publisher.Mono; + +import java.io.IOException; +import java.net.InetAddress; +import java.net.InetSocketAddress; +import java.net.SocketAddress; +import java.net.StandardProtocolFamily; +import java.nio.ByteBuffer; +import java.nio.channels.ClosedByInterruptException; +import java.nio.channels.DatagramChannel; +import java.nio.channels.SelectionKey; +import java.nio.channels.Selector; +import java.time.Duration; +import java.util.Iterator; + +@Service +public class UPNPService implements Runnable +{ + private static final Logger log = LoggerFactory.getLogger(UPNPService.class); + + private static final String MCAST_IP = "239.255.255.250"; + private static final int MCAST_PORT = 1900; + private static final int MCAST_BUFFER_SEND_SIZE_MAX = 512; // this is the maximum size used by MiniUPNPd so better not use more + private static final int MCAST_BUFFER_RECV_SIZE = 1024; // also used by MiniUPNPd + + private static final int MCAST_MAX_WAIT_TIME = (int) Duration.ofSeconds(3).toMillis(); // time to wait for a router reply + private static final int MCAST_MAX_WAIT_SNOOZE = (int) Duration.ofMinutes(5).toMillis(); // time to wait if nothing has answered all requests + private static final int MCAST_DELAY_HINT = (int) Duration.ofSeconds(1).toSeconds(); // how long a router can delay its reply + + private static final int PORT_DURATION = (int) Duration.ofHours(1).toMillis(); // how long does a port mapping lasts + private static final int PORT_DURATION_ANTICIPATION = (int) Duration.ofMinutes(1).toMillis(); // when to kick in the refresh before it expires + + private static final String[] DEVICES = { + // IGD 1 + "urn:schemas-upnp-org:device:InternetGatewayDevice:1", + "urn:schemas-upnp-org:service:WANIPConnection:1", + "urn:schemas-upnp-org:device:WANDevice:1", + "urn:schemas-upnp-org:device:WANConnectionDevice:1", + "urn:schemas-upnp-org:service:WANPPPConnection:1", + // IGD 2 + "urn:schemas-upnp-org:device:InternetGatewayDevice:2", + "urn:schemas-upnp-org:device:WANDevice:2", + "urn:schemas-upnp-org:device:WANConnectionDevice:2", + "urn:schemas-upnp-org:service:WANIPConnection:2", + // Most routers will respond to all entries + }; + + private enum State + { + SNOOZING, + BROADCASTING, + WAITING, + CONNECTING, + CONNECTED, + INTERRUPTED + } + + private final ConfigClient configClient; + + private int deviceIndex; + + private String localIpAddress; + private int localPort; + private Thread thread; + + private SocketAddress multicastAddress; + private ByteBuffer sendBuffer; + private ByteBuffer receiveBuffer; + private State state; + private Device device; + + public UPNPService(ConfigClient configClient) + { + this.configClient = configClient; + } + + public void start(String localIpAddress, int localPort) + { + log.info("Starting UPNP..."); + this.localIpAddress = localIpAddress; + this.localPort = localPort; + thread = new Thread(this, "UPNP Service"); + thread.start(); + } + + public void stop() + { + if (thread != null) + { + log.info("Stopping UPNP..."); + thread.interrupt(); + } + } + + public boolean isRunning() + { + return thread.isAlive(); + } + + public void waitForTermination() + { + if (thread != null) + { + try + { + log.info("Waiting for UPNP service to terminate..."); + thread.join(); + } + catch (InterruptedException e) + { + log.error("Failed to wait for termination: {}", e.getMessage(), e); + Thread.currentThread().interrupt(); + } + } + } + + private String getMSearch(String device) + { + return "M-SEARCH * HTTP/1.1\r\nHost: " + MCAST_IP + ":" + MCAST_PORT + "\r\nST: " + device + "\r\nMan: \"ssdp:discover\"\r\nMX: " + MCAST_DELAY_HINT + "\r\n\r\n"; + } + + private void getUpnpDeviceSearch(SelectionKey selectionKey) + { + sendBuffer = ByteBuffer.wrap(getMSearch(DEVICES[deviceIndex % DEVICES.length]).getBytes()); + if (sendBuffer.limit() > MCAST_BUFFER_SEND_SIZE_MAX) + { + throw new IllegalArgumentException("Send buffer bigger than " + MCAST_BUFFER_SEND_SIZE_MAX + " (" + sendBuffer.limit() + ")"); + } + deviceIndex++; + if (deviceIndex > 0 && deviceIndex % DEVICES.length == 0) + { + setState(State.SNOOZING, selectionKey); + } + } + + @Override + public void run() + { + multicastAddress = new InetSocketAddress(MCAST_IP, MCAST_PORT); + receiveBuffer = ByteBuffer.allocate(MCAST_BUFFER_RECV_SIZE); + + try (var selector = Selector.open(); + DatagramChannel channel = DatagramChannel.open(StandardProtocolFamily.INET) + .bind(new InetSocketAddress(InetAddress.getByName(localIpAddress), 0)) + ) + { + channel.configureBlocking(false); + var registerSelectionKeys = channel.register(selector, SelectionKey.OP_WRITE); + state = State.BROADCASTING; + + while (true) + { + if (state == State.BROADCASTING) + { + getUpnpDeviceSearch(registerSelectionKeys); + } + + selector.select(getSelectorTimeout()); + if (Thread.interrupted()) + { + setState(State.INTERRUPTED, registerSelectionKeys); + break; + } + if (state == State.CONNECTED) + { + boolean refreshed = refreshPorts(); + if (!refreshed) + { + log.error("UPNP port refresh failed, starting again..."); + setState(State.BROADCASTING, registerSelectionKeys); + continue; + } + } + handleSelection(selector, registerSelectionKeys); + } + + if (device != null && device.hasControlPoint()) + { + device.removeAllPortMapping(); + } + } + catch (ClosedByInterruptException e) + { + log.debug("Interrupted, bailing out..."); + } + catch (IOException e) + { + log.error("Error: ", e); + } + } + + private void handleSelection(Selector selector, SelectionKey registerSelectionKeys) + { + Iterator selectedKeys = selector.selectedKeys().iterator(); + if (!selectedKeys.hasNext() && state != State.CONNECTED) + { + setState(State.BROADCASTING, registerSelectionKeys); + } + while (selectedKeys.hasNext()) + { + try + { + SelectionKey key = selectedKeys.next(); + selectedKeys.remove(); + + if (!key.isValid()) + { + continue; + } + + if (key.isReadable()) + { + read(key); + } + else if (key.isWritable()) + { + write(key); + } + } + catch (IOException e) + { + log.error("Glitch, continuing...", e); // XXX: I think I should keep that part in case there's a transient network error. need experimenting + } + } + } + + private int getSelectorTimeout() + { + return switch (state) + { + case WAITING -> MCAST_MAX_WAIT_TIME; + case SNOOZING -> MCAST_MAX_WAIT_SNOOZE; + case CONNECTED -> PORT_DURATION - PORT_DURATION_ANTICIPATION; + default -> 0; + }; + } + + private void setState(State newState, SelectionKey key) + { + state = newState; + + switch (state) + { + case BROADCASTING -> key.interestOps(SelectionKey.OP_WRITE); + case WAITING -> key.interestOps(SelectionKey.OP_READ); + case CONNECTING, CONNECTED, SNOOZING -> key.interestOps(0); + case INTERRUPTED -> log.debug("Interrupted"); + } + } + + private void read(SelectionKey key) throws IOException + { + assert state == State.WAITING; + + DatagramChannel channel = (DatagramChannel) key.channel(); + SocketAddress routerAddress = channel.receive(receiveBuffer); // XXX: handle multiple responses if there's several routers. use 'rootdevice' to test + device = Device.from(routerAddress, receiveBuffer); + if (device.isValid()) + { + setState(State.CONNECTING, key); + device.addControlPoint(); + + if (device.hasControlPoint()) + { + setState(State.CONNECTED, key); + boolean added = refreshPorts(); // XXX: what to do if we failed? report to user? retry? + if (added) + { + log.info("UPNP ports added successfully."); + } + else + { + log.warn("Failed to add UPNP ports. {} might not accept incoming connections.", AppName.NAME); + } + findExternalIpAddress(); + } + else + { + // Device has no control point or it's unreachable, keep searching + setState(State.WAITING, key); + } + } + else + { + // Device has no location or address, keep searching + setState(State.WAITING, key); + } + + // XXX: a device must be blacklisted for a while if the above 2 steps fail for it, otherwise we'll run into it again if the user has a broken router on the same LAN + receiveBuffer.clear(); // ready to read again + } + + private void write(SelectionKey key) throws IOException + { + assert state == State.BROADCASTING; + + DatagramChannel channel = (DatagramChannel) key.channel(); + channel.send(sendBuffer, multicastAddress); + setState(State.WAITING, key); + sendBuffer.clear(); + } + + private boolean refreshPorts() + { + // XXX: add a mechanism if the localport is already taken on the router? + boolean refreshed = device.addPortMapping(localIpAddress, localPort, localPort, PORT_DURATION / 1000, Protocol.TCP); + refreshed &= device.addPortMapping(localIpAddress, localPort, localPort, PORT_DURATION / 1000, Protocol.UDP); + return refreshed; + } + + private void findExternalIpAddress() + { + String externalIpAddress = device.getExternalIpAddress(); + if (IP.isPublicIp(externalIpAddress)) + { + Mono result = configClient.updateExternalIpAddress(externalIpAddress, localPort); + result.subscribe(); + } + } +} diff --git a/app/src/main/java/io/xeres/app/net/upnp/package-info.java b/app/src/main/java/io/xeres/app/net/upnp/package-info.java new file mode 100644 index 000000000..8f99fa6a3 --- /dev/null +++ b/app/src/main/java/io/xeres/app/net/upnp/package-info.java @@ -0,0 +1,40 @@ +/* + * Copyright (c) 2019-2020 by David Gerber - https://zapek.com + * + * This file is part of Xeres. + * + * Xeres is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Xeres is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Xeres. If not, see . + */ + +/** + * UPNP implementation. + *

+ * This is a limited UPNP implementation that finds an active router on the network and sets the + * proper port forwarding. There is no active listening capabilities (eg. detecting if some new device was turned on) + * because the use cases for it are limited and it directly clashes with the OS (for example Windows + * is already listening on port 1900). Using the OS' UPNP stack would require the use of JNI on Windows, Linux + * has too many possible setups and OSX is unknown. + *

+ * The goal of this implementation is to be fast and useful in 99% of cases. + *

+ * Theory of operation: + *

    + *
  • UPNPService launches a thread
  • + *
  • the thread broadcasts a MSEARCH HTTPu query as multicast on port 1900
  • + *
  • a router answers with its location URL
  • + *
  • the thread connects to the control point and retrieves the control point URL which is described in an XML file
  • + *
  • further commands (add mapping, removing mapping, get external ip address) are sent to that control point URL using SOAP
  • + *
+ */ +package io.xeres.app.net.upnp; diff --git a/app/src/main/java/io/xeres/app/properties/DatabaseProperties.java b/app/src/main/java/io/xeres/app/properties/DatabaseProperties.java new file mode 100644 index 000000000..e6c0fb95e --- /dev/null +++ b/app/src/main/java/io/xeres/app/properties/DatabaseProperties.java @@ -0,0 +1,40 @@ +/* + * Copyright (c) 2019-2021 by David Gerber - https://zapek.com + * + * This file is part of Xeres. + * + * Xeres is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Xeres is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Xeres. If not, see . + */ + +package io.xeres.app.properties; + +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.context.annotation.Configuration; + +@Configuration +@ConfigurationProperties(prefix = "xrs.db") +public class DatabaseProperties +{ + private Integer cacheSize; + + public Integer getCacheSize() + { + return cacheSize; + } + + public void setCacheSize(Integer cacheSize) + { + this.cacheSize = cacheSize; + } +} diff --git a/app/src/main/java/io/xeres/app/properties/NetworkProperties.java b/app/src/main/java/io/xeres/app/properties/NetworkProperties.java new file mode 100644 index 000000000..2115a80aa --- /dev/null +++ b/app/src/main/java/io/xeres/app/properties/NetworkProperties.java @@ -0,0 +1,91 @@ +/* + * Copyright (c) 2019-2020 by David Gerber - https://zapek.com + * + * This file is part of Xeres. + * + * Xeres is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Xeres is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Xeres. If not, see . + */ + +package io.xeres.app.properties; + +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.context.annotation.Configuration; + +import javax.annotation.PostConstruct; + +@Configuration +@ConfigurationProperties(prefix = "xrs.network") +public class NetworkProperties +{ + /** + * Enables the slicing of packets. This is only available on new Retroshare packets and only if both ends + * of the connection agree to use them. Note that Xeres always accepts sliced packets. + */ + private boolean packetSlicing = false; + + /** + * Enables the grouping of packets. Only works if packet slicing is enabled. + */ + private boolean packetGrouping = false; + + /** + * Enables the DHT (Mainline DHT). + */ + private boolean dht = false; + + @PostConstruct + private void checkConsistency() + { + if (packetGrouping && !packetSlicing) + { + throw new IllegalStateException("'network.packet-grouping' property cannot be enabled without 'network.packet-slicing'"); + } + } + + public String getFeatures() + { + return "packet slicing: " + packetSlicing + ", " + + "packet grouping: " + packetGrouping; + } + + public boolean isPacketSlicing() + { + return packetSlicing; + } + + public void setPacketSlicing(boolean packetSlicing) + { + this.packetSlicing = packetSlicing; + } + + public boolean isPacketGrouping() + { + return packetGrouping; + } + + public void setPacketGrouping(boolean packetGrouping) + { + this.packetGrouping = packetGrouping; + } + + public boolean isDht() + { + return dht; + } + + public void setDht(boolean dht) + { + this.dht = dht; + } +} diff --git a/app/src/main/java/io/xeres/app/properties/UiProperties.java b/app/src/main/java/io/xeres/app/properties/UiProperties.java new file mode 100644 index 000000000..7618a9aac --- /dev/null +++ b/app/src/main/java/io/xeres/app/properties/UiProperties.java @@ -0,0 +1,64 @@ +/* + * Copyright (c) 2019-2020 by David Gerber - https://zapek.com + * + * This file is part of Xeres. + * + * Xeres is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Xeres is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Xeres. If not, see . + */ + +package io.xeres.app.properties; + +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.context.annotation.Configuration; + +@Configuration +@ConfigurationProperties(prefix = "xrs.ui") +public class UiProperties +{ + private boolean enabled = true; + + private String address; + + private int port = 1066; + + public boolean isEnabled() + { + return enabled; + } + + public void setEnabled(boolean enabled) + { + this.enabled = enabled; + } + + public String getAddress() + { + return address; + } + + public void setAddress(String address) + { + this.address = address; + } + + public int getPort() + { + return port; + } + + public void setPort(int port) + { + this.port = port; + } +} diff --git a/app/src/main/java/io/xeres/app/service/ChatRoomService.java b/app/src/main/java/io/xeres/app/service/ChatRoomService.java new file mode 100644 index 000000000..09b7c1846 --- /dev/null +++ b/app/src/main/java/io/xeres/app/service/ChatRoomService.java @@ -0,0 +1,86 @@ +/* + * Copyright (c) 2019-2021 by David Gerber - https://zapek.com + * + * This file is part of Xeres. + * + * Xeres is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Xeres is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Xeres. If not, see . + */ + +package io.xeres.app.service; + +import io.xeres.app.database.model.chatroom.ChatRoom; +import io.xeres.app.database.model.identity.Identity; +import io.xeres.app.database.repository.ChatRoomRepository; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.util.List; +import java.util.Optional; + +@Service +@Transactional(readOnly = true) +public class ChatRoomService +{ + private final ChatRoomRepository chatRoomRepository; + + public ChatRoomService(ChatRoomRepository chatRoomRepository) + { + this.chatRoomRepository = chatRoomRepository; + } + + @Transactional + public ChatRoom createChatRoom(io.xeres.app.xrs.service.chat.ChatRoom chatRoom, Identity identity) + { + return chatRoomRepository.save(ChatRoom.createChatRoom(chatRoom, identity)); + } + + @Transactional + public ChatRoom subscribeToChatRoomAndJoin(io.xeres.app.xrs.service.chat.ChatRoom chatRoom, Identity identity) + { + var entity = chatRoomRepository.findByRoomIdAndIdentity(chatRoom.getId(), identity).orElseGet(() -> createChatRoom(chatRoom, identity)); + entity.setSubscribed(true); + entity.setJoined(true); + return chatRoomRepository.save(entity); + } + + @Transactional + public ChatRoom unsubscribeFromChatRoomAndLeave(long chatRoomId, Identity identity) + { + Optional foundRoom = chatRoomRepository.findByRoomIdAndIdentity(chatRoomId, identity); + + foundRoom.ifPresent(subscribedRoom -> { + subscribedRoom.setSubscribed(false); + subscribedRoom.setJoined(false); + chatRoomRepository.save(subscribedRoom); + }); + return foundRoom.orElse(null); + } + + @Transactional + public void deleteChatRoom(long chatRoomId, Identity identity) + { + chatRoomRepository.findByRoomIdAndIdentity(chatRoomId, identity).ifPresent(chatRoomRepository::delete); + } + + public List getAllChatRoomsPendingToSubscribe() + { + return chatRoomRepository.findAllBySubscribedTrueAndJoinedFalse(); + } + + @Transactional + public void markAllChatRoomsAsLeft() + { + chatRoomRepository.putAllJoinedToFalse(); + } +} diff --git a/app/src/main/java/io/xeres/app/service/GxsExchangeService.java b/app/src/main/java/io/xeres/app/service/GxsExchangeService.java new file mode 100644 index 000000000..5b8a8d025 --- /dev/null +++ b/app/src/main/java/io/xeres/app/service/GxsExchangeService.java @@ -0,0 +1,78 @@ +/* + * Copyright (c) 2019-2021 by David Gerber - https://zapek.com + * + * This file is part of Xeres. + * + * Xeres is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Xeres is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Xeres. If not, see . + */ + +package io.xeres.app.service; + +import io.xeres.app.database.model.gxs.GxsClientUpdate; +import io.xeres.app.database.model.gxs.GxsServiceSetting; +import io.xeres.app.database.model.location.Location; +import io.xeres.app.database.repository.GxsClientUpdateRepository; +import io.xeres.app.database.repository.GxsServiceSettingRepository; +import io.xeres.app.xrs.service.RsServiceType; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.time.Instant; +import java.time.temporal.ChronoUnit; + +@Service +@Transactional(readOnly = true) +public class GxsExchangeService +{ + private final GxsClientUpdateRepository gxsClientUpdateRepository; + private final GxsServiceSettingRepository gxsServiceSettingRepository; + + public GxsExchangeService(GxsClientUpdateRepository gxsClientUpdateRepository, GxsServiceSettingRepository gxsServiceSettingRepository) + { + this.gxsClientUpdateRepository = gxsClientUpdateRepository; + this.gxsServiceSettingRepository = gxsServiceSettingRepository; + } + + public Instant getLastPeerUpdate(Location location, RsServiceType serviceType) + { + return gxsClientUpdateRepository.findByLocationAndServiceType(location, serviceType.getType()) + .map(GxsClientUpdate::getLastSynced) + .orElse(Instant.EPOCH).truncatedTo(ChronoUnit.SECONDS); + } + + public void setLastPeerUpdate(Location location, RsServiceType serviceType, Instant now) + { + gxsClientUpdateRepository.findByLocationAndServiceType(location, serviceType.getType()) + .ifPresentOrElse(gxsClientUpdate -> { + gxsClientUpdate.setLastSynced(now); + gxsClientUpdateRepository.save(gxsClientUpdate); + }, () -> gxsClientUpdateRepository.save(new GxsClientUpdate(location, serviceType.getType(), now))); + } + + public Instant getLastServiceUpdate(RsServiceType serviceType) + { + return gxsServiceSettingRepository.findById(serviceType.getType()) + .map(GxsServiceSetting::getLastUpdated) + .orElse(Instant.EPOCH).truncatedTo(ChronoUnit.SECONDS); + } + + public void setLastServiceUpdate(RsServiceType serviceType, Instant now) + { + gxsServiceSettingRepository.findById(serviceType.getType()) + .ifPresentOrElse(gxsServiceSetting -> { + gxsServiceSetting.setLastUpdated(now); + gxsServiceSettingRepository.save(gxsServiceSetting); + }, () -> gxsServiceSettingRepository.save(new GxsServiceSetting(serviceType.getType(), now))); + } +} diff --git a/app/src/main/java/io/xeres/app/service/IdentityService.java b/app/src/main/java/io/xeres/app/service/IdentityService.java new file mode 100644 index 000000000..6d95f7d7a --- /dev/null +++ b/app/src/main/java/io/xeres/app/service/IdentityService.java @@ -0,0 +1,205 @@ +/* + * Copyright (c) 2019-2021 by David Gerber - https://zapek.com + * + * This file is part of Xeres. + * + * Xeres is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Xeres is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Xeres. If not, see . + */ + +package io.xeres.app.service; + +import io.xeres.app.crypto.pgp.PGP; +import io.xeres.app.crypto.pgp.PGP.Armor; +import io.xeres.app.crypto.rsa.RSA; +import io.xeres.app.database.model.gxs.GxsCircleType; +import io.xeres.app.database.model.gxs.GxsPrivacyFlags; +import io.xeres.app.database.model.identity.Identity; +import io.xeres.app.database.repository.GxsIdRepository; +import io.xeres.app.database.repository.IdentityRepository; +import io.xeres.app.xrs.service.RsServiceType; +import io.xeres.app.xrs.service.gxsid.item.GxsIdGroupItem; +import io.xeres.common.dto.identity.IdentityConstants; +import io.xeres.common.id.GxsId; +import io.xeres.common.id.Id; +import io.xeres.common.id.ProfileFingerprint; +import io.xeres.common.id.Sha1Sum; +import io.xeres.common.identity.Type; +import org.bouncycastle.crypto.Digest; +import org.bouncycastle.crypto.digests.SHA1Digest; +import org.bouncycastle.openpgp.PGPException; +import org.bouncycastle.openpgp.PGPSecretKey; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.math.BigInteger; +import java.security.NoSuchAlgorithmException; +import java.security.cert.CertificateException; +import java.security.interfaces.RSAPrivateKey; +import java.security.spec.InvalidKeySpecException; +import java.util.Arrays; +import java.util.EnumSet; + +@Service +@Transactional(readOnly = true) +public class IdentityService +{ + private static final Logger log = LoggerFactory.getLogger(IdentityService.class); + + private final IdentityRepository identityRepository; + private final GxsIdRepository gxsIdRepository; + private final PrefsService prefsService; + private final ProfileService profileService; + private final GxsExchangeService gxsExchangeService; + + public IdentityService(IdentityRepository identityRepository, GxsIdRepository gxsIdRepository, PrefsService prefsService, ProfileService profileService, GxsExchangeService gxsExchangeService) + { + this.identityRepository = identityRepository; + this.gxsIdRepository = gxsIdRepository; + this.prefsService = prefsService; + this.profileService = profileService; + this.gxsExchangeService = gxsExchangeService; + } + + @Transactional + public long createOwnIdentity(String name, Type type) throws CertificateException, PGPException, IOException + { + if (!prefsService.isOwnProfilePresent()) + { + throw new CertificateException("Cannot create an identity without a profile; Create a profile first"); + } + if (!prefsService.hasOwnLocation()) + { + throw new IllegalArgumentException("Cannot create an identity without a location; Create a location first"); + } + log.debug("Creating identity key..."); + //RsGenExchange::publishGroup(); with a RsGxsIdGroupItem() + + var keyPair = RSA.generateKeys(2048); + var rsaPrivateKey = (RSAPrivateKey) keyPair.getPrivate(); + + var gxsId = makeGxsId( + getAsOneComplement(rsaPrivateKey.getModulus()), + getAsOneComplement(rsaPrivateKey.getPrivateExponent())); + + var gxsIdGroupItem = new GxsIdGroupItem(gxsId, name); + gxsIdGroupItem.setAdminPrivateKeyData(keyPair.getPrivate().getEncoded()); + gxsIdGroupItem.setAdminPublicKeyData(keyPair.getPublic().getEncoded()); + + gxsIdGroupItem.setCircleType(GxsCircleType.PUBLIC); + + if (type == Type.SIGNED) + { + var hash = makeProfileHash(gxsId, profileService.getOwnProfile().getProfileFingerprint()); + gxsIdGroupItem.setProfileHash(hash); + gxsIdGroupItem.setProfileSignature(makeProfileSignature(PGP.getPGPSecretKey(prefsService.getSecretProfileKey()), hash)); + + // This is because of some backward compatibility, ideally it should be PUBLIC | REAL_ID + // PRIVATE is equal to READ_ID_deprecated + gxsIdGroupItem.setDiffusionFlags(EnumSet.of(GxsPrivacyFlags.PRIVATE, GxsPrivacyFlags.READ_ID)); + } + else + { + gxsIdGroupItem.setDiffusionFlags(EnumSet.of(GxsPrivacyFlags.PUBLIC)); + } + + gxsIdGroupItem = gxsIdRepository.save(gxsIdGroupItem); + + var ownIdentity = Identity.createOwnIdentity(gxsIdGroupItem, type); + Identity saved = identityRepository.save(ownIdentity); + + gxsExchangeService.setLastServiceUpdate(RsServiceType.GXSID, gxsIdGroupItem.getPublished()); + + return saved.getId(); + } + + public Identity getOwnIdentity() // XXX: temporary, we'll have several identites later + { + return identityRepository.findById(IdentityConstants.OWN_IDENTITY_ID).orElseThrow(() -> new IllegalStateException("Missing own identity")); + } + + public void saveIdentity(Identity identity) + { + // XXX: important! there should be some checks to make sure there's no malicious overwrite (probably a simple validation should do as id == fingerprint of key) + var gxsIdGroup = gxsIdRepository.findByGxsId(identity.getGxsIdGroupItem().getGxsId()).orElse(identity.getGxsIdGroupItem()); + gxsIdRepository.save(gxsIdGroup); + + if (identity.isNotable()) + { + identityRepository.save(identity); + } + } + + public byte[] signData(Identity identity, byte[] data) + { + try + { + return RSA.sign(data, RSA.getPrivateKey(identity.getGxsIdGroupItem().getAdminPrivateKeyData())); + } + catch (NoSuchAlgorithmException e) + { + throw new IllegalStateException("No such algorithm: " + e.getMessage()); + } + catch (InvalidKeySpecException e) + { + throw new IllegalArgumentException("Invalid key spec: " + e.getMessage()); + } + } + + private byte[] getAsOneComplement(BigInteger number) + { + byte[] array = number.toByteArray(); + if (array[0] == 0) + { + array = Arrays.copyOfRange(array, 1, array.length); + } + return array; + } + + private GxsId makeGxsId(byte[] modulus, byte[] exponent) + { + var sha1sum = new byte[Sha1Sum.LENGTH]; + + Digest digest = new SHA1Digest(); + digest.update(modulus, 0, modulus.length); + digest.update(exponent, 0, exponent.length); + digest.doFinal(sha1sum, 0); + + // Copy the first 16 bytes of the sha1 sum to get the GxsId + return new GxsId(Arrays.copyOfRange(sha1sum, 0, GxsId.LENGTH)); + } + + private Sha1Sum makeProfileHash(GxsId gxsId, ProfileFingerprint fingerprint) + { + var sha1sum = new byte[Sha1Sum.LENGTH]; + var gxsIdAsciiUpper = Id.toAsciiBytesUpperCase(gxsId); + + Digest digest = new SHA1Digest(); + digest.update(gxsIdAsciiUpper, 0, gxsIdAsciiUpper.length); + digest.update(fingerprint.getBytes(), 0, fingerprint.getLength()); + digest.doFinal(sha1sum, 0); + return new Sha1Sum(sha1sum); + } + + private byte[] makeProfileSignature(PGPSecretKey pgpSecretKey, Sha1Sum hashToSign) throws PGPException, IOException + { + var out = new ByteArrayOutputStream(); + PGP.sign(pgpSecretKey, new ByteArrayInputStream(hashToSign.getBytes()), out, Armor.NONE); + return out.toByteArray(); + } +} diff --git a/app/src/main/java/io/xeres/app/service/LocationService.java b/app/src/main/java/io/xeres/app/service/LocationService.java new file mode 100644 index 000000000..bef387b8c --- /dev/null +++ b/app/src/main/java/io/xeres/app/service/LocationService.java @@ -0,0 +1,346 @@ +/* + * Copyright (c) 2019-2021 by David Gerber - https://zapek.com + * + * This file is part of Xeres. + * + * Xeres is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Xeres is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Xeres. If not, see . + */ + +package io.xeres.app.service; + +import io.xeres.app.application.events.LocationReadyEvent; +import io.xeres.app.crypto.pgp.PGP; +import io.xeres.app.crypto.rsa.RSA; +import io.xeres.app.crypto.rsid.RSSerialVersion; +import io.xeres.app.crypto.x509.X509; +import io.xeres.app.database.model.connection.Connection; +import io.xeres.app.database.model.location.Location; +import io.xeres.app.database.repository.LocationRepository; +import io.xeres.app.net.protocol.PeerAddress; +import io.xeres.common.id.LocationId; +import io.xeres.common.properties.StartupProperties; +import io.xeres.common.protocol.NetMode; +import io.xeres.common.protocol.ip.IP; +import org.apache.commons.lang3.StringUtils; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.context.ApplicationEventPublisher; +import org.springframework.data.domain.PageRequest; +import org.springframework.data.domain.Slice; +import org.springframework.data.domain.Sort; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.io.IOException; +import java.net.InetAddress; +import java.net.InetSocketAddress; +import java.net.SocketAddress; +import java.net.UnknownHostException; +import java.security.NoSuchAlgorithmException; +import java.security.cert.CertificateException; +import java.security.spec.InvalidKeySpecException; +import java.time.Instant; +import java.util.*; +import java.util.concurrent.CompletableFuture; +import java.util.function.Predicate; + +import static io.xeres.common.dto.location.LocationConstants.OWN_LOCATION_ID; + +@Service +@Transactional(readOnly = true) +public class LocationService +{ + private static final Logger log = LoggerFactory.getLogger(LocationService.class); + + private static final int KEY_SIZE = 3072; + + private static final int CONNECTION_POOL_SIZE = 10; // number of locations to connect at once + + public enum UpdateConnectionStatus + { + UPDATED, + ADDED + } + + private final PrefsService prefsService; + private final ProfileService profileService; + private final LocationRepository locationRepository; + private final ApplicationEventPublisher publisher; + + private Slice locations; + private int pageIndex; + private int connectionIndex = -1; + + public LocationService(PrefsService prefsService, ProfileService profileService, LocationRepository locationRepository, ApplicationEventPublisher publisher) + { + this.prefsService = prefsService; + this.profileService = profileService; + this.locationRepository = locationRepository; + this.publisher = publisher; + } + + void generateLocationKeys() + { + if (prefsService.getLocationPrivateKeyData() != null) + { + return; + } + + log.info("Generating keys, algorithm: RSA, bits: {} ...", KEY_SIZE); + + var keyPair = RSA.generateKeys(KEY_SIZE); + + log.info("Successfully generated key pair"); + + prefsService.saveLocationKeys(keyPair); + } + + void generateLocationCertificate() throws CertificateException, InvalidKeySpecException, NoSuchAlgorithmException, IOException + { + if (prefsService.hasOwnLocation()) + { + return; + } + if (!prefsService.isOwnProfilePresent()) + { + throw new CertificateException("Cannot generate certificate without a profile; Create a profile first"); + } + + log.info("Generating certificate..."); + + var x509Certificate = X509.generateCertificate( + PGP.getPGPSecretKey(prefsService.getSecretProfileKey()), + RSA.getPublicKey(prefsService.getLocationPublicKeyData()), + "CN=" + Long.toHexString(profileService.getOwnProfile().getPgpIdentifier()).toUpperCase(Locale.ROOT), // older RS use a random string I think, like 12:34:55:44:4e:44:99:23 + "CN=-", + new Date(0), + new Date(0), + RSSerialVersion.V07_0001.serialNumber() + ); + + log.info("Successfully generated certificate"); + + prefsService.saveLocationCertificate(x509Certificate.getEncoded()); + } + + @Transactional + public void createOwnLocation(String name) throws CertificateException + { + if (!prefsService.isOwnProfilePresent()) + { + throw new CertificateException("Cannot create a location without a profile; Create a profile first"); + } + var ownProfile = profileService.getOwnProfile(); + + if (!ownProfile.getLocations().isEmpty()) + { + throw new CertificateException("Location already exists"); + } + + String localIpAddress = Optional.ofNullable(IP.getLocalIpAddress()).orElseThrow(() -> new CertificateException("Current host has no IP address. Please configure your network")); + + // Create an IPv4 location (XXX: add other protocols later) + int localPort = Optional.ofNullable(StartupProperties.getInteger(StartupProperties.Property.SERVER_PORT)).orElseGet(IP::getFreeLocalPort); + log.info("Using local ip address {} and port {}", localIpAddress, localPort); + + generateLocationKeys(); + + try + { + generateLocationCertificate(); + } + catch (InvalidKeySpecException | NoSuchAlgorithmException | IOException e) + { + throw new CertificateException("Failed to generate certificate: " + e.getMessage()); + } + + var location = Location.createLocation(name); + location.setLocationId(prefsService.getLocationId()); + ownProfile.addLocation(location); + locationRepository.save(location); + + // Send the event asynchronously so that our transaction can complete first + CompletableFuture.runAsync(() -> publisher.publishEvent(new LocationReadyEvent(localIpAddress, localPort))); + } + + /** + * Find the location. + * + * @param locationId the SSL identifier + * @return the location + */ + public Optional findLocationById(LocationId locationId) + { + return locationRepository.findByLocationId(locationId); + } + + public Optional findOwnLocation() + { + return locationRepository.findById(OWN_LOCATION_ID); + } + + public Optional findLocationById(long id) + { + return locationRepository.findById(id); + } + + @Transactional + public void markAllConnectionsAsDisconnected() + { + locationRepository.putAllConnectedToFalse(); + } + + @Transactional + public void setConnected(Location location, SocketAddress socketAddress) + { + updateConnection(location, socketAddress); // XXX: is this the right place? maybe it should be done in discovery service + + location.setConnected(true); + locationRepository.save(location); + } + + private void updateConnection(Location location, SocketAddress socketAddress) + { + var inetSocketAddress = (InetSocketAddress) socketAddress; + + location.getConnections().stream() + .filter(conn -> conn.getAddress().split(":")[0].equals(inetSocketAddress.getHostString())) + .findFirst() + .ifPresent(connection -> connection.setLastConnected(Instant.now())); + } + + @Transactional + public void setDisconnected(Location location) + { + location.setConnected(false); + locationRepository.save(location); + } + + @Transactional + public Location update(Location location, String locationName, NetMode netMode, String version, boolean discoverable, boolean dht, List peerAddresses, String hostname) + { + location.setName(locationName); + location.setNetMode(netMode); + location.setVersion(version); + location.setDiscoverable(discoverable); + location.setDht(dht); + peerAddresses.forEach(peerAddress -> updateConnection(location, peerAddress)); + // XXX: missing hostname. where's the hostname support?! is it in the connection? I don't think that's the right place for it... should be the location + return locationRepository.save(location); + } + + public List getConnectionsToConnectTo() + { + locations = locationRepository.findAllByConnectedFalse(PageRequest.of(getPageIndex(), getPageSize(), Sort.by("lastConnected").descending())); // XXX: check if the sorting works + + return locations.stream() + .filter(Predicate.not(Location::isOwn)) + .flatMap(location -> location.getBestConnection(getConnectionIndex())) + .limit(CONNECTION_POOL_SIZE) + .toList(); + } + + public List getConnectedLocations() + { + return locationRepository.findAllByConnectedTrue(); + } + + @Transactional + public UpdateConnectionStatus updateConnection(Location location, PeerAddress peerAddress) + { + var updated = false; + + if (location.isOwn()) + { + for (Connection connection : location.getConnections()) + { + updated = updateConnectionIfSameType(peerAddress, connection); + if (updated) + { + break; + } + } + + } + else + { + for (Connection connection : location.getConnections()) + { + if (peerAddress.getType() == connection.getType() + && peerAddress.getAddress().orElseThrow().equals(connection.getAddress())) + { + updated = true; + break; + } + } + + } + if (!updated) + { + location.addConnection(Connection.from(peerAddress)); + } + locationRepository.save(location); + return updated ? UpdateConnectionStatus.UPDATED : UpdateConnectionStatus.ADDED; + } + + public String getHostname() throws UnknownHostException + { + return InetAddress.getLocalHost().getHostName(); + } + + public String getUsername() + { + String username = System.getProperty("user.name"); + if (StringUtils.isEmpty(username)) + { + throw new NoSuchElementException("No logged in username"); + } + return username; + } + + private boolean updateConnectionIfSameType(PeerAddress from, Connection to) + { + if ((from.isExternal() && to.isExternal()) + || (!from.isExternal() && !to.isExternal())) + { + to.setAddress(from.getAddress().orElseThrow()); + return true; + } + return false; + } + + private int getPageIndex() + { + if (locations == null || locations.isLast()) + { + pageIndex = 0; + connectionIndex++; + } + else + { + pageIndex++; + } + return pageIndex; + } + + private int getPageSize() + { + return CONNECTION_POOL_SIZE; // XXX: make it dynamic depending on the connection speed and reliability + } + + private int getConnectionIndex() + { + return connectionIndex; + } +} diff --git a/app/src/main/java/io/xeres/app/service/PeerService.java b/app/src/main/java/io/xeres/app/service/PeerService.java new file mode 100644 index 000000000..d44cea7b0 --- /dev/null +++ b/app/src/main/java/io/xeres/app/service/PeerService.java @@ -0,0 +1,85 @@ +/* + * Copyright (c) 2019-2020 by David Gerber - https://zapek.com + * + * This file is part of Xeres. + * + * Xeres is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Xeres is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Xeres. If not, see . + */ + +package io.xeres.app.service; + +import io.netty.util.concurrent.DefaultEventExecutorGroup; +import io.netty.util.concurrent.EventExecutorGroup; +import io.xeres.app.net.peer.bootstrap.PeerClient; +import io.xeres.app.net.peer.bootstrap.PeerServer; +import io.xeres.common.properties.StartupProperties; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.stereotype.Service; + +@Service +public class PeerService +{ + private static final Logger log = LoggerFactory.getLogger(PeerService.class); + + private static final int SSL_THREADS = 4; // XXX: is this ok? we probably don't need many since it's only for the first SSL handshake + private static final int HANDLER_THREADS = 8; // XXX: ponder how much we should have for the most common setup + + private final PeerClient peerClient; + private final PeerServer peerServer; + + private EventExecutorGroup sslExecutorGroup; + private EventExecutorGroup handlerExecutorGroup; + + public PeerService(PeerClient peerClient, PeerServer peerServer) + { + this.peerClient = peerClient; + this.peerServer = peerServer; + } + + public void start() + { + sslExecutorGroup = new DefaultEventExecutorGroup(SSL_THREADS); + handlerExecutorGroup = new DefaultEventExecutorGroup(HANDLER_THREADS); + + peerServer.start(sslExecutorGroup, handlerExecutorGroup); + if (!StartupProperties.getBoolean(StartupProperties.Property.SERVER_ONLY, false)) + { + peerClient.start(sslExecutorGroup, handlerExecutorGroup); + } + } + + public void stop() + { + peerServer.stop(); + peerClient.stop(); + try + { + if (sslExecutorGroup != null) + { + sslExecutorGroup.shutdownGracefully().sync(); + } + + if (handlerExecutorGroup != null) + { + handlerExecutorGroup.shutdownGracefully().sync(); + } + } + catch (InterruptedException e) + { + log.error("Error while shutting down executor group: {}", e.getMessage()); + Thread.currentThread().interrupt(); + } + } +} diff --git a/app/src/main/java/io/xeres/app/service/PrefsService.java b/app/src/main/java/io/xeres/app/service/PrefsService.java new file mode 100644 index 000000000..26b010b3b --- /dev/null +++ b/app/src/main/java/io/xeres/app/service/PrefsService.java @@ -0,0 +1,125 @@ +/* + * Copyright (c) 2019-2021 by David Gerber - https://zapek.com + * + * This file is part of Xeres. + * + * Xeres is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Xeres is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Xeres. If not, see . + */ + +package io.xeres.app.service; + +import io.xeres.app.crypto.x509.X509; +import io.xeres.app.database.model.prefs.Prefs; +import io.xeres.app.database.repository.PrefsRepository; +import io.xeres.common.id.LocationId; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import javax.annotation.PostConstruct; +import java.security.KeyPair; +import java.security.cert.CertificateException; +import java.security.cert.X509Certificate; + +@Service +@Transactional(readOnly = true) +public class PrefsService +{ + private final PrefsRepository prefsRepository; + + private Prefs prefs; + + public PrefsService(PrefsRepository prefsRepository) + { + this.prefsRepository = prefsRepository; + } + + @PostConstruct + void init() + { + prefs = prefsRepository.findById((byte) 1).orElseThrow(() -> new IllegalStateException("No setting configuration")); + } + + @Transactional + public void save() + { + prefsRepository.save(prefs); + } + + public void backup(String file) + { + prefsRepository.backupDatabase(file); + } + + // XXX: I think those need 'synchronized' or so... depends how we use them + public void saveSecretProfileKey(byte[] privateKeyData) + { + prefs.setPgpPrivateKeyData(privateKeyData); + save(); + } + + public byte[] getSecretProfileKey() + { + return prefs.getPgpPrivateKeyData(); + } + + public void saveLocationKeys(KeyPair keyPair) + { + prefs.setLocationPrivateKeyData(keyPair.getPrivate().getEncoded()); + prefs.setLocationPublicKeyData(keyPair.getPublic().getEncoded()); + save(); + } + + public byte[] getLocationPublicKeyData() + { + return prefs.getLocationPublicKeyData(); + } + + public byte[] getLocationPrivateKeyData() + { + return prefs.getLocationPrivateKeyData(); + } + + public void saveLocationCertificate(byte[] data) + { + prefs.setLocationCertificate(data); + save(); + } + + public X509Certificate getLocationCertificate() + { + try + { + return X509.getCertificate(prefs.getLocationCertificate()); + } + catch (CertificateException e) + { + throw new IllegalStateException("Certificate is corrupt"); // Can't happen + } + } + + public LocationId getLocationId() throws CertificateException + { + return X509.getLocationId(getLocationCertificate()); + } + + public boolean hasOwnLocation() + { + return prefs.hasLocationCertificate(); + } + + public boolean isOwnProfilePresent() + { + return prefs.getPgpPrivateKeyData() != null; + } +} diff --git a/app/src/main/java/io/xeres/app/service/ProfileService.java b/app/src/main/java/io/xeres/app/service/ProfileService.java new file mode 100644 index 000000000..9b7769c0d --- /dev/null +++ b/app/src/main/java/io/xeres/app/service/ProfileService.java @@ -0,0 +1,214 @@ +/* + * Copyright (c) 2019-2021 by David Gerber - https://zapek.com + * + * This file is part of Xeres. + * + * Xeres is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Xeres is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Xeres. If not, see . + */ + +package io.xeres.app.service; + +import io.xeres.app.crypto.pgp.PGP; +import io.xeres.app.crypto.rsid.RSId; +import io.xeres.app.crypto.rsid.RSId.Type; +import io.xeres.app.database.model.location.Location; +import io.xeres.app.database.model.profile.Profile; +import io.xeres.app.database.repository.ProfileRepository; +import io.xeres.common.AppName; +import io.xeres.common.dto.profile.ProfileConstants; +import io.xeres.common.id.Id; +import io.xeres.common.id.LocationId; +import io.xeres.common.id.ProfileFingerprint; +import org.bouncycastle.jce.provider.BouncyCastleProvider; +import org.bouncycastle.openpgp.PGPException; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.io.IOException; +import java.security.Security; +import java.security.cert.CertificateException; +import java.util.List; +import java.util.Optional; + +@Service +@Transactional(readOnly = true) +public class ProfileService +{ + private static final Logger log = LoggerFactory.getLogger(ProfileService.class); + + private static final int KEY_SIZE = 3072; + private static final int KEY_ID_LENGTH_MIN = ProfileConstants.NAME_LENGTH_MIN; + private static final int KEY_ID_LENGTH_MAX = ProfileConstants.NAME_LENGTH_MAX; + private static final String KEY_ID_SUFFIX = "(Generated by " + AppName.NAME + ")"; + + private final ProfileRepository profileRepository; + private final PrefsService prefsService; + + public ProfileService(ProfileRepository profileRepository, PrefsService prefsService) + { + this.profileRepository = profileRepository; + this.prefsService = prefsService; + + Security.addProvider(new BouncyCastleProvider()); + } + + @Transactional + public boolean generateProfileKeys(String name) + { + if (prefsService.getSecretProfileKey() != null) + { + throw new IllegalStateException("Private profile key already exists"); + } + + if (name.length() < KEY_ID_LENGTH_MIN) + { + throw new IllegalArgumentException("Key ID is too short, minimum is " + KEY_ID_LENGTH_MIN); + } + + if (name.length() + KEY_ID_SUFFIX.length() + 1 > KEY_ID_LENGTH_MAX) + { + throw new IllegalArgumentException("Key ID is too long, maximum is " + (KEY_ID_LENGTH_MAX - KEY_ID_SUFFIX.length())); + } + + log.info("Generating PGP keys, algorithm: RSA, bits: {} ...", KEY_SIZE); + + try + { + var pgpSecretKey = PGP.generateSecretKey(name, KEY_ID_SUFFIX, KEY_SIZE); + var pgpPublicKey = pgpSecretKey.getPublicKey(); + + log.info("Successfully generated PGP key pair, id: {}", Id.toString(pgpSecretKey.getKeyID())); + + var ownProfile = Profile.createOwnProfile(name, pgpPublicKey.getKeyID(), new ProfileFingerprint(pgpPublicKey.getFingerprint()), pgpPublicKey.getEncoded()); + profileRepository.save(ownProfile); + prefsService.saveSecretProfileKey(pgpSecretKey.getEncoded()); + return true; + } + catch (PGPException | IOException e) + { + log.error("Failed to generate PGP key pair", e); + } + return false; + } + + public Profile getOwnProfile() + { + return profileRepository.findById(ProfileConstants.OWN_PROFILE_ID).orElseThrow(() -> new IllegalStateException("Missing own profile")); + } + + public Optional findProfileById(long id) + { + return profileRepository.findById(id); + } + + public Optional findProfileByName(String name) + { + return profileRepository.findByName(name); + } + + public Optional findProfileByPgpFingerprint(ProfileFingerprint profileFingerprint) + { + return profileRepository.findByProfileFingerprint(profileFingerprint); + } + + public Optional findProfileByPgpIdentifier(long pgpIdentifier) + { + return profileRepository.findByPgpIdentifier(pgpIdentifier); + } + + public Optional findDiscoverableProfileByPgpIdentifier(long pgpIdentifier) + { + return profileRepository.findDiscoverableProfileByPgpIdentifier(pgpIdentifier); + } + + public Optional findProfileByLocationId(LocationId locationId) + { + return profileRepository.findProfileByLocationId(locationId); + } + + @Transactional + public Optional createOrUpdateProfile(Profile profile) + { + Optional savedProfile = findProfileByPgpFingerprint(profile.getProfileFingerprint()); + if (savedProfile.isPresent()) + { + profile = savedProfile.get().updateWith(profile); + } + + try + { + return Optional.of(profileRepository.save(profile)); + } + catch (IllegalArgumentException e) + { + return Optional.empty(); + } + } + + public Profile getProfileFromRSId(RSId rsId, Type type) throws CertificateException + { + if (rsId.hasPgpPublicKey()) + { + if (type == Type.SHORT_INVITE) + { + throw new CertificateException("Old certificates are deprecated. Only ShortInvites are accepted."); + } + + var key = rsId.getPgpPublicKey(); + Profile profile; + try + { + profile = Profile.createProfile(key.getUserIDs().next(), key.getKeyID(), new ProfileFingerprint(key.getFingerprint()), key.getEncoded()); + profile.setAccepted(true); + } + catch (IOException e) + { + throw new IllegalStateException("The PGP provider is seriously broken"); // can't happen since we just parsed it + } + + if (rsId.hasLocationInfo()) + { + profile.addLocation(Location.createLocation(rsId)); + } + return profile; + } + else + { + var profileFingerprint = new ProfileFingerprint(rsId.getPgpFingerprint()); + var profile = findProfileByPgpFingerprint(profileFingerprint).orElseGet(() -> Profile.createEmptyProfile(rsId.getName(), rsId.getPgpIdentifier(), profileFingerprint)); + profile.setAccepted(true); + profile.addLocation(Location.createLocation(rsId)); + return profile; + } + } + + @Transactional + public void deleteProfile(long id) + { + var profile = profileRepository.findById(id).orElseThrow(); + profileRepository.delete(profile); + } + + public List getAllProfiles() + { + return profileRepository.findAll(); + } + + public List getAllDiscoverableProfiles() + { + return profileRepository.getAllDiscoverableProfiles(); + } +} diff --git a/app/src/main/java/io/xeres/app/web/api/DefaultHandler.java b/app/src/main/java/io/xeres/app/web/api/DefaultHandler.java new file mode 100644 index 000000000..c608ae537 --- /dev/null +++ b/app/src/main/java/io/xeres/app/web/api/DefaultHandler.java @@ -0,0 +1,89 @@ +/* + * Copyright (c) 2019-2021 by David Gerber - https://zapek.com + * + * This file is part of Xeres. + * + * Xeres is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Xeres is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Xeres. If not, see . + */ + +package io.xeres.app.web.api; + +import io.swagger.v3.oas.annotations.OpenAPIDefinition; +import io.swagger.v3.oas.annotations.info.Contact; +import io.swagger.v3.oas.annotations.info.Info; +import io.swagger.v3.oas.annotations.info.License; +import io.xeres.app.web.api.error.Error; +import io.xeres.app.web.api.error.ErrorResponseEntity; +import io.xeres.app.web.api.error.exception.UnprocessableEntityException; +import io.xeres.common.AppName; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.ExceptionHandler; +import org.springframework.web.bind.annotation.RestControllerAdvice; + +import javax.persistence.EntityNotFoundException; +import java.net.UnknownHostException; +import java.util.NoSuchElementException; + +@RestControllerAdvice +@OpenAPIDefinition( + info = @Info( + title = AppName.NAME + " API definition", + version = "0.1", + description = "This is the REST API available for UI clients.", + license = @License(name = "GPL v3", url = "https://www.gnu.org/licenses/gpl-3.0.en.html"), + contact = @Contact(url = "https://zapek.com", name = "David Gerber", email = "info@zapek.com") + ) +) +public class DefaultHandler +{ + private static final Logger log = LoggerFactory.getLogger(DefaultHandler.class); + + @ExceptionHandler({ + NoSuchElementException.class, + EntityNotFoundException.class, + UnknownHostException.class}) + public ResponseEntity handleNotFoundException(Exception e) + { + log.error("Exception: {}, {}", e.getClass().getCanonicalName(), e.getMessage()); + var builder = new ErrorResponseEntity.Builder(HttpStatus.NOT_FOUND) + .setError("No such entity") + .setException(e); + + return builder.build(); + } + + @ExceptionHandler(UnprocessableEntityException.class) + public ResponseEntity handleUnprocessableEntityException(UnprocessableEntityException e) + { + log.error("Exception: {}, {}", e.getClass().getCanonicalName(), e.getMessage()); + return new ErrorResponseEntity.Builder(HttpStatus.UNPROCESSABLE_ENTITY) + .setError(e.getMessage()) + .setId(e.getId()) + .setException(e.getCause()) + .build(); + } + + @ExceptionHandler(RuntimeException.class) + public ResponseEntity handleRuntimeException(RuntimeException e) + { + log.error("RuntimeException: {}, {}", e.getClass().getCanonicalName(), e.getMessage(), e); + return new ErrorResponseEntity.Builder(HttpStatus.INTERNAL_SERVER_ERROR) + .setError(e.getMessage()) + .setException(e.getCause()) + .build(); + } +} diff --git a/app/src/main/java/io/xeres/app/web/api/controller/chat/ChatController.java b/app/src/main/java/io/xeres/app/web/api/controller/chat/ChatController.java new file mode 100644 index 000000000..be48eab3c --- /dev/null +++ b/app/src/main/java/io/xeres/app/web/api/controller/chat/ChatController.java @@ -0,0 +1,79 @@ +/* + * Copyright (c) 2019-2021 by David Gerber - https://zapek.com + * + * This file is part of Xeres. + * + * Xeres is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Xeres is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Xeres. If not, see . + */ + +package io.xeres.app.web.api.controller.chat; + +import io.swagger.v3.oas.annotations.ExternalDocumentation; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.headers.Header; +import io.swagger.v3.oas.annotations.media.Schema; +import io.swagger.v3.oas.annotations.responses.ApiResponse; +import io.swagger.v3.oas.annotations.tags.Tag; +import io.xeres.app.xrs.service.chat.ChatService; +import io.xeres.app.xrs.service.chat.RoomFlags; +import io.xeres.common.rest.chat.CreateChatRoomRequest; +import org.springframework.http.HttpStatus; +import org.springframework.http.MediaType; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.*; +import org.springframework.web.servlet.support.ServletUriComponentsBuilder; + +import javax.validation.Valid; +import java.util.EnumSet; + +import static io.xeres.common.rest.PathConfig.CHAT_PATH; + +@Tag(name = "Chat", description = "Chat service", externalDocs = @ExternalDocumentation(url = "https://xeres.io/docs/api/chat", description = "Chat documentation")) +@RestController +@RequestMapping(value = CHAT_PATH, produces = MediaType.APPLICATION_JSON_VALUE) +public class ChatController +{ + private final ChatService chatService; + + public ChatController(ChatService chatService) + { + this.chatService = chatService; + } + + @PostMapping("/rooms") + @Operation(summary = "Create a chat room") + @ApiResponse(responseCode = "201", description = "Room created successfully", headers = @Header(name = "Room", description = "The location of the created room", schema = @Schema(type = "string"))) + public ResponseEntity createChatRoom(@Valid @RequestBody CreateChatRoomRequest createChatRoomRequest) + { + long id = chatService.createChatRoom(createChatRoomRequest.name(), createChatRoomRequest.topic(), null, EnumSet.of(RoomFlags.PUBLIC)); // XXX: fix arguments + + var location = ServletUriComponentsBuilder.fromCurrentRequest().replacePath("/rooms/{id}").buildAndExpand(id).toUri(); + return ResponseEntity.created(location).build(); + } + + @PutMapping("/rooms/{id}/subscription") + @ResponseStatus(HttpStatus.OK) + public long subscribeToChatRoom(@PathVariable long id) + { + chatService.joinChatRoom(id); // XXX: error if we're already subscribed + return id; + } + + @DeleteMapping("/rooms/{id}/subscription") + @ResponseStatus(HttpStatus.NO_CONTENT) + public void unsubscribeFromChatRoom(@PathVariable long id) + { + chatService.leaveChatRoom(id); // XXX: error if we're not subscribed + } +} diff --git a/app/src/main/java/io/xeres/app/web/api/controller/chat/ChatMessageController.java b/app/src/main/java/io/xeres/app/web/api/controller/chat/ChatMessageController.java new file mode 100644 index 000000000..4c1dbd911 --- /dev/null +++ b/app/src/main/java/io/xeres/app/web/api/controller/chat/ChatMessageController.java @@ -0,0 +1,96 @@ +/* + * Copyright (c) 2019-2021 by David Gerber - https://zapek.com + * + * This file is part of Xeres. + * + * Xeres is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Xeres is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Xeres. If not, see . + */ + +package io.xeres.app.web.api.controller.chat; + +import io.xeres.app.xrs.service.chat.ChatService; +import io.xeres.common.id.LocationId; +import io.xeres.common.message.MessageType; +import io.xeres.common.message.chat.ChatMessage; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.messaging.handler.annotation.MessageExceptionHandler; +import org.springframework.messaging.handler.annotation.MessageMapping; +import org.springframework.messaging.handler.annotation.Payload; +import org.springframework.messaging.simp.SimpMessageHeaderAccessor; +import org.springframework.messaging.simp.annotation.SendToUser; +import org.springframework.stereotype.Controller; + +import javax.validation.Valid; + +import static io.xeres.common.message.MessageHeaders.DESTINATION_ID; +import static io.xeres.common.message.MessageHeaders.MESSAGE_TYPE; +import static io.xeres.common.rest.PathConfig.CHAT_PATH; + +@Controller +public class ChatMessageController +{ + private static final Logger log = LoggerFactory.getLogger(ChatMessageController.class); + + private final ChatService chatService; + + public ChatMessageController(ChatService chatService) + { + this.chatService = chatService; + } + + @MessageMapping(CHAT_PATH) + public void processMessageFromClient(SimpMessageHeaderAccessor accessor, @Payload @Valid ChatMessage message) + { + String destinationId = accessor.getFirstNativeHeader(DESTINATION_ID); + var messageType = MessageType.valueOf(accessor.getFirstNativeHeader(MESSAGE_TYPE)); + + switch (messageType) + { + case CHAT_PRIVATE_MESSAGE -> { + assert destinationId != null; + log.debug("Received websocket message, sending to peer location: {}, content {}", destinationId, message); + chatService.sendPrivateMessage(new LocationId(destinationId), message.getContent()); + } + case CHAT_ROOM_MESSAGE -> { + assert destinationId != null; + log.debug("Sending to room: {}, content {}", destinationId, message); + chatService.sendChatRoomMessage(Long.parseLong(destinationId), message.getContent()); + } + case CHAT_BROADCAST_MESSAGE -> { + log.debug("Sending broadcast message"); + chatService.sendBroadcastMessage(message.getContent()); + } + case CHAT_TYPING_NOTIFICATION -> { + assert destinationId != null; + log.debug("Sending typing notification..."); + chatService.sendPrivateTypingNotification(new LocationId(destinationId)); + } + case CHAT_ROOM_TYPING_NOTIFICATION -> { + assert destinationId != null; + log.debug("Sending typing notification..."); + chatService.sendChatRoomTypingNotification(Long.parseLong(destinationId)); + } + default -> log.error("Couldn't figure out which message to send"); + } + } + + @MessageExceptionHandler + @SendToUser("/queue/errors") // XXX: how can we use this? Well, it works... just have to subscribe to it + public String handleException(Throwable e) + { + log.debug("Got exception: {}", e.getMessage(), e); + return e.getMessage(); + } +} diff --git a/app/src/main/java/io/xeres/app/web/api/controller/config/ConfigController.java b/app/src/main/java/io/xeres/app/web/api/controller/config/ConfigController.java new file mode 100644 index 000000000..c93692c85 --- /dev/null +++ b/app/src/main/java/io/xeres/app/web/api/controller/config/ConfigController.java @@ -0,0 +1,192 @@ +/* + * Copyright (c) 2019-2021 by David Gerber - https://zapek.com + * + * This file is part of Xeres. + * + * Xeres is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Xeres is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Xeres. If not, see . + */ + +package io.xeres.app.web.api.controller.config; + +import io.swagger.v3.oas.annotations.ExternalDocumentation; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.headers.Header; +import io.swagger.v3.oas.annotations.media.Content; +import io.swagger.v3.oas.annotations.media.Schema; +import io.swagger.v3.oas.annotations.responses.ApiResponse; +import io.swagger.v3.oas.annotations.tags.Tag; +import io.xeres.app.database.model.connection.Connection; +import io.xeres.app.net.protocol.PeerAddress; +import io.xeres.app.service.IdentityService; +import io.xeres.app.service.LocationService; +import io.xeres.app.service.ProfileService; +import io.xeres.app.web.api.error.Error; +import io.xeres.app.web.api.error.exception.InternalServerErrorException; +import io.xeres.common.rest.config.*; +import org.bouncycastle.openpgp.PGPException; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.http.HttpStatus; +import org.springframework.http.MediaType; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.*; +import org.springframework.web.servlet.support.ServletUriComponentsBuilder; + +import javax.validation.Valid; +import java.io.IOException; +import java.net.UnknownHostException; +import java.security.cert.CertificateException; + +import static io.xeres.common.identity.Type.ANONYMOUS; +import static io.xeres.common.identity.Type.SIGNED; +import static io.xeres.common.rest.PathConfig.*; + +@Tag(name = "Configuration", description = "Runtime general configuration", externalDocs = @ExternalDocumentation(url = "https://xeres.io/docs/api/config", description = "Configuration documentation")) +@RestController +@RequestMapping(value = CONFIG_PATH, produces = MediaType.APPLICATION_JSON_VALUE) +public class ConfigController +{ + private static final Logger log = LoggerFactory.getLogger(ConfigController.class); + + private final ProfileService profileService; + private final LocationService locationService; + private final IdentityService identityService; + + public ConfigController(ProfileService profileService, LocationService locationService, IdentityService identityService) + { + this.profileService = profileService; + this.locationService = locationService; + this.identityService = identityService; + } + + @PostMapping("/profile") + @Operation(summary = "Create own profile") + @ApiResponse(responseCode = "201", description = "Profile created successfully", headers = @Header(name = "Location", description = "The location of the created profile", schema = @Schema(type = "string"))) + @ApiResponse(responseCode = "422", description = "Profile entity cannot be processed", content = @Content(schema = @Schema(implementation = Error.class))) + @ApiResponse(responseCode = "500", description = "Serious error", content = @Content(schema = @Schema(implementation = Error.class))) + public ResponseEntity createOwnProfile(@Valid @RequestBody OwnProfileRequest ownProfileRequest) + { + String name = ownProfileRequest.name(); + log.debug("Processing creation of Profile {}", name); + + if (!profileService.generateProfileKeys(name)) + { + throw new InternalServerErrorException("Failed to generate profile keys"); + } + var location = ServletUriComponentsBuilder.fromCurrentRequest().replacePath(PROFILES_PATH + "/{id}").buildAndExpand(1L).toUri(); + return ResponseEntity.created(location).build(); + } + + @PostMapping("/location") + @Operation(summary = "Create own location") + @ApiResponse(responseCode = "201", description = "Location created successfully") + @ApiResponse(responseCode = "500", description = "Serious error", content = @Content(schema = @Schema(implementation = Error.class))) + @ResponseStatus(HttpStatus.CREATED) + public void createLocation(@Valid @RequestBody OwnLocationRequest ownLocationRequest) + { + String name = ownLocationRequest.name(); + log.debug("Processing creation of Location {}", name); + + try + { + locationService.createOwnLocation(name); + } + catch (CertificateException e) + { + throw new InternalServerErrorException("Failed to generate location: " + e.getMessage()); + } + } + + @PostMapping("/identity") + @Operation(summary = "Create own identity") + @ApiResponse(responseCode = "201", description = "Identity created successfully") + @ApiResponse(responseCode = "500", description = "Serious error", content = @Content(schema = @Schema(implementation = Error.class))) + public ResponseEntity createOwnIdentity(@Valid @RequestBody OwnIdentityRequest ownIdentityRequest) + { + String name = ownIdentityRequest.name(); + log.debug("Creating identity {}", name); + long id; + + try + { + id = identityService.createOwnIdentity(name, ownIdentityRequest.anonymous() ? ANONYMOUS : SIGNED); + } + catch (CertificateException | PGPException | IOException e) + { + throw new InternalServerErrorException("Failed to generate identity: " + e.getMessage()); + } + var location = ServletUriComponentsBuilder.fromCurrentRequest().replacePath(IDENTITY_PATH + "/{id}").buildAndExpand(id).toUri(); + return ResponseEntity.created(location).build(); + } + + @PutMapping("/externalIp") + @Operation(summary = "Set or update the external IP address and port.", description = "Note that an external IP address is not strictly required if for example the host is on a public IP already.") + @ApiResponse(responseCode = "201", description = "IP address set successfully", headers = @Header(name = "Location", description = "The location of where to get the IP address", schema = @Schema(type = "string"))) + @ApiResponse(responseCode = "204", description = "IP address updated successfully") + public ResponseEntity updateExternalIpAddress(@Valid @RequestBody IpAddressRequest request) + { + log.info("External IP address: {}", request); + var peerAddress = PeerAddress.from(request.ip(), request.port()); + if (!peerAddress.isExternal()) + { + throw new IllegalArgumentException("Wrong external IP address"); + } + + switch (locationService.updateConnection(locationService.findOwnLocation().orElseThrow(), peerAddress)) + { + case ADDED: + var location = ServletUriComponentsBuilder.fromCurrentRequest().build().toUri(); + return ResponseEntity.created(location).build(); + + case UPDATED: + return ResponseEntity.noContent().build(); + + default: + throw new IllegalStateException(); + } + } + + @GetMapping("/externalIp") + @Operation(summary = "Get the external IP address and port.", description = "Note that an external IP address is not strictly required if for example the host is on a public IP already.") + @ApiResponse(responseCode = "200", description = "Request successful") + @ApiResponse(responseCode = "404", description = "No location or no external IP address", content = @Content(schema = @Schema(implementation = Error.class))) + public IpAddressResponse getExternalIpAddress() + { + var connection = locationService.findOwnLocation().orElseThrow() + .getConnections() + .stream() + .filter(Connection::isExternal) + .findFirst().orElseThrow(); + + return new IpAddressResponse(connection.getIp(), connection.getPort()); + } + + @GetMapping("/hostname") + @Operation(summary = "Get the machine's hostname.") + @ApiResponse(responseCode = "200", description = "Request successful") + @ApiResponse(responseCode = "404", description = "No hostname (host configuration problem)", content = @Content(schema = @Schema(implementation = Error.class))) + public HostnameResponse getHostname() throws UnknownHostException + { + return new HostnameResponse(locationService.getHostname()); + } + + @GetMapping("/username") + @Operation(summary = "Get the OS session's username.") + @ApiResponse(responseCode = "200", description = "Reqeust successful") + @ApiResponse(responseCode = "404", description = "No username (no user session)", content = @Content(schema = @Schema(implementation = Error.class))) + public UsernameResponse getUsername() + { + return new UsernameResponse(locationService.getUsername()); + } +} diff --git a/app/src/main/java/io/xeres/app/web/api/controller/connection/ConnectionController.java b/app/src/main/java/io/xeres/app/web/api/controller/connection/ConnectionController.java new file mode 100644 index 000000000..be5e2d434 --- /dev/null +++ b/app/src/main/java/io/xeres/app/web/api/controller/connection/ConnectionController.java @@ -0,0 +1,62 @@ +/* + * Copyright (c) 2019-2021 by David Gerber - https://zapek.com + * + * This file is part of Xeres. + * + * Xeres is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Xeres is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Xeres. If not, see . + */ + +package io.xeres.app.web.api.controller.connection; + +import io.swagger.v3.oas.annotations.ExternalDocumentation; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.responses.ApiResponse; +import io.swagger.v3.oas.annotations.tags.Tag; +import io.xeres.app.database.model.location.Location; +import io.xeres.app.service.LocationService; +import io.xeres.common.dto.profile.ProfileDTO; +import org.springframework.http.MediaType; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +import java.util.List; + +import static io.xeres.app.database.model.profile.ProfileMapper.toDeepDTOs; +import static io.xeres.common.rest.PathConfig.CONNECTIONS_PATH; +import static java.util.function.Predicate.not; + +@Tag(name = "Connection", description = "Connected peers", externalDocs = @ExternalDocumentation(url = "https://xeres.io/docs/api/connection", description = "Connection documentation")) +@RestController +@RequestMapping(value = CONNECTIONS_PATH, produces = MediaType.APPLICATION_JSON_VALUE) +public class ConnectionController +{ + private final LocationService locationService; + + public ConnectionController(LocationService locationService) + { + this.locationService = locationService; + } + + @GetMapping("/profiles") + @Operation(summary = "Get all connected profiles") + @ApiResponse(responseCode = "200", description = "Request completed successfully") + public List getConnectedProfiles() + { + return toDeepDTOs(locationService.getConnectedLocations().stream() + .filter(not(Location::isOwn)) + .map(Location::getProfile) + .toList()); + } +} diff --git a/app/src/main/java/io/xeres/app/web/api/controller/location/LocationController.java b/app/src/main/java/io/xeres/app/web/api/controller/location/LocationController.java new file mode 100644 index 000000000..bca784569 --- /dev/null +++ b/app/src/main/java/io/xeres/app/web/api/controller/location/LocationController.java @@ -0,0 +1,76 @@ +/* + * Copyright (c) 2019-2021 by David Gerber - https://zapek.com + * + * This file is part of Xeres. + * + * Xeres is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Xeres is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Xeres. If not, see . + */ + +package io.xeres.app.web.api.controller.location; + +import io.swagger.v3.oas.annotations.ExternalDocumentation; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.media.Content; +import io.swagger.v3.oas.annotations.media.Schema; +import io.swagger.v3.oas.annotations.responses.ApiResponse; +import io.swagger.v3.oas.annotations.tags.Tag; +import io.xeres.app.crypto.rsid.RSIdArmor; +import io.xeres.app.service.LocationService; +import io.xeres.app.web.api.error.Error; +import io.xeres.common.dto.location.LocationDTO; +import io.xeres.common.id.LocationId; +import io.xeres.common.rest.location.RSIdResponse; +import org.springframework.http.MediaType; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +import java.io.IOException; + +import static io.xeres.app.database.model.location.LocationMapper.toDTO; +import static io.xeres.common.rest.PathConfig.LOCATIONS_PATH; + +@Tag(name = "Location", description = "Local instance", externalDocs = @ExternalDocumentation(url = "https://xeres.io/docs/api/location", description = "Location documentation")) +@RestController +@RequestMapping(value = LOCATIONS_PATH, produces = MediaType.APPLICATION_JSON_VALUE) +public class LocationController +{ + private final LocationService locationService; + + public LocationController(LocationService locationService) + { + this.locationService = locationService; + } + + @GetMapping("/{id}") // XXX: justify the use of this endpoint + @Operation(summary = "Return a location") + @ApiResponse(responseCode = "200", description = "Location found") + @ApiResponse(responseCode = "404", description = "Location not found", content = @Content(schema = @Schema(implementation = Error.class))) + public LocationDTO findLocationById(@PathVariable String id) + { + return toDTO(locationService.findLocationById(new LocationId(id)).orElseThrow()); + } + + @GetMapping("/{id}/rsid") + @Operation(summary = "Return a location's RSId") + @ApiResponse(responseCode = "200", description = "Location found") + @ApiResponse(responseCode = "404", description = "Profile not found", content = @Content(schema = @Schema(implementation = Error.class))) + public RSIdResponse getRSIdOfLocationId(@PathVariable long id) throws IOException + { + var location = locationService.findLocationById(id).orElseThrow(); + + return new RSIdResponse(RSIdArmor.getArmored(location.getRSId())); + } +} diff --git a/app/src/main/java/io/xeres/app/web/api/controller/notification/NotificationController.java b/app/src/main/java/io/xeres/app/web/api/controller/notification/NotificationController.java new file mode 100644 index 000000000..2f3eb7534 --- /dev/null +++ b/app/src/main/java/io/xeres/app/web/api/controller/notification/NotificationController.java @@ -0,0 +1,59 @@ +/* + * Copyright (c) 2019-2021 by David Gerber - https://zapek.com + * + * This file is part of Xeres. + * + * Xeres is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Xeres is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Xeres. If not, see . + */ + +package io.xeres.app.web.api.controller.notification; + +import io.swagger.v3.oas.annotations.ExternalDocumentation; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.responses.ApiResponse; +import io.swagger.v3.oas.annotations.tags.Tag; +import io.xeres.app.web.api.sse.SsePushNotificationService; +import org.springframework.http.MediaType; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; +import org.springframework.web.servlet.mvc.method.annotation.SseEmitter; + +import static io.xeres.common.rest.PathConfig.NOTIFICATIONS_PATH; + +@Tag(name = "Notification", description = "Out of band notifications", externalDocs = @ExternalDocumentation(url = "https://xeres.io/docs/api/notification", description = "Notification documentation")) +@RestController +@RequestMapping(value = NOTIFICATIONS_PATH, produces = MediaType.APPLICATION_JSON_VALUE) +public class NotificationController +{ + private final SsePushNotificationService ssePushNotificationService; + + public NotificationController(SsePushNotificationService ssePushNotificationService) + { + this.ssePushNotificationService = ssePushNotificationService; + } + + @GetMapping + @Operation(summary = "Subscribe to notifications") + @ApiResponse(responseCode = "200", description = "Request completed successfully") + public SseEmitter setupNotification() + { + var emitter = new SseEmitter(); + ssePushNotificationService.addEmitter(emitter); + emitter.onCompletion(() -> ssePushNotificationService.removeEmitter(emitter)); + emitter.onTimeout(() -> ssePushNotificationService.removeEmitter(emitter)); + + return emitter; + } +} diff --git a/app/src/main/java/io/xeres/app/web/api/controller/profile/ProfileController.java b/app/src/main/java/io/xeres/app/web/api/controller/profile/ProfileController.java new file mode 100644 index 000000000..bc7e60899 --- /dev/null +++ b/app/src/main/java/io/xeres/app/web/api/controller/profile/ProfileController.java @@ -0,0 +1,154 @@ +/* + * Copyright (c) 2019-2021 by David Gerber - https://zapek.com + * + * This file is part of Xeres. + * + * Xeres is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Xeres is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Xeres. If not, see . + */ + +package io.xeres.app.web.api.controller.profile; + +import io.swagger.v3.oas.annotations.ExternalDocumentation; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.headers.Header; +import io.swagger.v3.oas.annotations.media.Content; +import io.swagger.v3.oas.annotations.media.Schema; +import io.swagger.v3.oas.annotations.responses.ApiResponse; +import io.swagger.v3.oas.annotations.tags.Tag; +import io.xeres.app.crypto.rsid.RSId; +import io.xeres.app.database.model.profile.Profile; +import io.xeres.app.service.ProfileService; +import io.xeres.app.web.api.error.Error; +import io.xeres.app.web.api.error.exception.UnprocessableEntityException; +import io.xeres.common.dto.profile.ProfileDTO; +import io.xeres.common.id.LocationId; +import io.xeres.common.rest.profile.CertificateRequest; +import org.springframework.http.HttpStatus; +import org.springframework.http.MediaType; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.*; +import org.springframework.web.servlet.support.ServletUriComponentsBuilder; + +import javax.validation.Valid; +import java.security.cert.CertificateException; +import java.util.Collections; +import java.util.List; +import java.util.Optional; + +import static io.xeres.app.crypto.rsid.RSId.Type.BOTH; +import static io.xeres.app.crypto.rsid.RSId.Type.SHORT_INVITE; +import static io.xeres.app.database.model.profile.ProfileMapper.*; +import static io.xeres.common.rest.PathConfig.PROFILES_PATH; +import static org.apache.commons.lang3.StringUtils.isNotBlank; + +@Tag(name = "Profile", description = "User's profiles", externalDocs = @ExternalDocumentation(url = "https://xeres.io/docs/api/profile", description = "Profile documentation")) +@RestController +@RequestMapping(value = PROFILES_PATH, produces = MediaType.APPLICATION_JSON_VALUE) +public class ProfileController +{ + private final ProfileService profileService; + + public ProfileController(ProfileService profileService) + { + this.profileService = profileService; + } + + @GetMapping("/{id}") + @Operation(summary = "Return a profile") + @ApiResponse(responseCode = "200", description = "Profile found") + @ApiResponse(responseCode = "404", description = "Profile not found", content = @Content(schema = @Schema(implementation = Error.class))) + public ProfileDTO findProfileById(@PathVariable long id) + { + return toDTO(profileService.findProfileById(id).orElseThrow()); + } + + @GetMapping + @Operation(summary = "Search all profiles", description = "If no search parameters are provided, return all profiles") + @ApiResponse(responseCode = "200", description = "All matched profiles") + public List findProfiles( + @RequestParam(value = "name", required = false) String name, + @RequestParam(value = "locationId", required = false) String locationId) + { + if (isNotBlank(name)) + { + Optional profile = profileService.findProfileByName(name); + return profile.map(p -> List.of(toDTO(p))).orElse(Collections.emptyList()); + } + else if (isNotBlank(locationId)) + { + Optional profile = profileService.findProfileByLocationId(new LocationId(locationId)); + return profile.map(p -> List.of(toDTO(p))).orElse(Collections.emptyList()); + } + return toDTOs(profileService.getAllProfiles()); + } + + @PostMapping + @Operation(summary = "Create a profile and its possible location from a certificate") + @ApiResponse(responseCode = "201", description = "Profile created successfully", headers = @Header(name = "location", description = "the location of the profile")) + @ApiResponse(responseCode = "422", description = "Profile entity cannot be processed", content = @Content(schema = @Schema(implementation = Error.class))) + @ApiResponse(responseCode = "500", description = "Serious error", content = @Content(schema = @Schema(implementation = Error.class))) + public ResponseEntity createProfileFromCertificate(@Valid @RequestBody CertificateRequest certificateRequest) + { + Profile profile; + try + { + profile = profileService.getProfileFromRSId(RSId.parse(certificateRequest.certificate()), SHORT_INVITE); + } + catch (CertificateException e) + { + throw new UnprocessableEntityException("Couldn't parse certificate/shortinvite: " + e.getMessage()); + } + + var savedProfile = profileService.createOrUpdateProfile(profile).orElseThrow(() -> new UnprocessableEntityException("Failed to save profile")); + + var location = ServletUriComponentsBuilder.fromCurrentRequest().path("/{id}").buildAndExpand(savedProfile.getId()).toUri(); + return ResponseEntity.created(location).build(); + } + + @PostMapping("/check") + @Operation(summary = "Check a profile certificate") + @ApiResponse(responseCode = "200", description = "Profile certificate is OK") + @ApiResponse(responseCode = "422", description = "Profile certificate cannot be processed", content = @Content(schema = @Schema(implementation = Error.class))) + @ApiResponse(responseCode = "500", description = "Serious error", content = @Content(schema = @Schema(implementation = Error.class))) + public ResponseEntity checkProfileFromCertificate(@Valid @RequestBody CertificateRequest certificateRequest) + { + ProfileDTO profileDTO; + RSId rsId; + try + { + rsId = RSId.parse(certificateRequest.certificate()); + profileDTO = toDeepDTO(profileService.getProfileFromRSId(rsId, BOTH)); + } + catch (CertificateException e) + { + throw new UnprocessableEntityException("Couldn't parse certificate/shortinvite: " + e.getMessage()); + } + return ResponseEntity.ok() + .body(profileDTO); + } + + @DeleteMapping("/{id}") + @Operation(summary = "Delete a profile") + @ApiResponse(responseCode = "200", description = "Profile successfully deleted") + @ApiResponse(responseCode = "404", description = "Profile not found", content = @Content(schema = @Schema(implementation = Error.class))) + @ResponseStatus(HttpStatus.NO_CONTENT) + public void deleteProfile(@PathVariable long id) + { + if (Profile.isOwn(id)) + { + throw new UnprocessableEntityException("The main profile cannot be deleted"); + } + profileService.deleteProfile(id); + } +} diff --git a/app/src/main/java/io/xeres/app/web/api/error/Error.java b/app/src/main/java/io/xeres/app/web/api/error/Error.java new file mode 100644 index 000000000..5b77b2c7b --- /dev/null +++ b/app/src/main/java/io/xeres/app/web/api/error/Error.java @@ -0,0 +1,70 @@ +/* + * Copyright (c) 2019-2020 by David Gerber - https://zapek.com + * + * This file is part of Xeres. + * + * Xeres is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Xeres is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Xeres. If not, see . + */ + +package io.xeres.app.web.api.error; + +import com.fasterxml.jackson.annotation.JsonInclude; + +import java.util.ArrayList; +import java.util.List; + +public class Error +{ + @JsonInclude(JsonInclude.Include.NON_NULL) + private String contextId; + private String message; + private List details; + + public Error() + { + } + + public Error(String contextId, String message, Throwable throwable) + { + super(); + this.contextId = contextId; + this.message = message; + if (throwable != null && throwable.getMessage() != null) + { + this.details = List.of(throwable.getMessage()); + } + else + { + this.details = new ArrayList<>(); + } + } + + @SuppressWarnings("unused") + public String getContextId() + { + return contextId; + } + + @SuppressWarnings("unused") + public String getMessage() + { + return message; + } + + @SuppressWarnings("unused") + public List getDetails() + { + return details; + } +} diff --git a/app/src/main/java/io/xeres/app/web/api/error/ErrorResponseEntity.java b/app/src/main/java/io/xeres/app/web/api/error/ErrorResponseEntity.java new file mode 100644 index 000000000..3ff3970c4 --- /dev/null +++ b/app/src/main/java/io/xeres/app/web/api/error/ErrorResponseEntity.java @@ -0,0 +1,113 @@ +/* + * Copyright (c) 2019-2020 by David Gerber - https://zapek.com + * + * This file is part of Xeres. + * + * Xeres is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Xeres is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Xeres. If not, see . + */ + +package io.xeres.app.web.api.error; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; + +import java.util.Objects; + +public final class ErrorResponseEntity extends ResponseEntity +{ + private final Error error; + + private ErrorResponseEntity(Error error, HttpStatus httpStatus) + { + super(error, httpStatus); + this.error = error; + } + + public String getContextId() + { + return error.getContextId(); + } + + public String getMessage() + { + return error.getMessage(); + } + + @Override + public boolean equals(Object o) + { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + if (!super.equals(o)) return false; + ErrorResponseEntity that = (ErrorResponseEntity) o; + return Objects.equals(error, that.error); + } + + @Override + public int hashCode() + { + return Objects.hash(super.hashCode(), error); + } + + public static class Builder + { + private final HttpStatus httpStatus; + private String id; + private String error; + private Throwable exception; + + public Builder(HttpStatus httpStatus) + { + this.httpStatus = httpStatus; + } + + public Builder setId(String id) + { + this.id = id; + return this; + } + + public Builder setError(String error) + { + this.error = error; + return this; + } + + public Builder setException(Throwable exception) + { + this.exception = exception; + return this; + } + + public ErrorResponseEntity build() + { + return new ErrorResponseEntity(new Error(id, error, exception), httpStatus); + } + + public ErrorResponseEntity fromJson(String json) + { + var objectMapper = new ObjectMapper(); + try + { + return new ErrorResponseEntity(objectMapper.readValue(json, Error.class), httpStatus); + } + catch (JsonProcessingException e) + { + return new ErrorResponseEntity(new Error(null, null, null), httpStatus); // XXX: not sure those defaults are the best + } + } + } +} diff --git a/app/src/main/java/io/xeres/app/web/api/error/exception/InternalServerErrorException.java b/app/src/main/java/io/xeres/app/web/api/error/exception/InternalServerErrorException.java new file mode 100644 index 000000000..1e252f5e0 --- /dev/null +++ b/app/src/main/java/io/xeres/app/web/api/error/exception/InternalServerErrorException.java @@ -0,0 +1,28 @@ +/* + * Copyright (c) 2019-2020 by David Gerber - https://zapek.com + * + * This file is part of Xeres. + * + * Xeres is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Xeres is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Xeres. If not, see . + */ + +package io.xeres.app.web.api.error.exception; + +public class InternalServerErrorException extends RuntimeException +{ + public InternalServerErrorException(String message) + { + super(message); + } +} diff --git a/app/src/main/java/io/xeres/app/web/api/error/exception/UnprocessableEntityException.java b/app/src/main/java/io/xeres/app/web/api/error/exception/UnprocessableEntityException.java new file mode 100644 index 000000000..99c82f756 --- /dev/null +++ b/app/src/main/java/io/xeres/app/web/api/error/exception/UnprocessableEntityException.java @@ -0,0 +1,41 @@ +/* + * Copyright (c) 2019-2020 by David Gerber - https://zapek.com + * + * This file is part of Xeres. + * + * Xeres is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Xeres is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Xeres. If not, see . + */ + +package io.xeres.app.web.api.error.exception; + +public class UnprocessableEntityException extends RuntimeException +{ + private String id; + + public UnprocessableEntityException(String message) + { + super(message); + } + + public UnprocessableEntityException(String id, String message) + { + super(message); + this.id = id; + } + + public String getId() + { + return id; + } +} diff --git a/app/src/main/java/io/xeres/app/web/api/sse/SsePushNotificationService.java b/app/src/main/java/io/xeres/app/web/api/sse/SsePushNotificationService.java new file mode 100644 index 000000000..0d91d4652 --- /dev/null +++ b/app/src/main/java/io/xeres/app/web/api/sse/SsePushNotificationService.java @@ -0,0 +1,63 @@ +/* + * Copyright (c) 2019-2020 by David Gerber - https://zapek.com + * + * This file is part of Xeres. + * + * Xeres is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Xeres is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Xeres. If not, see . + */ + +package io.xeres.app.web.api.sse; + +import org.springframework.stereotype.Service; +import org.springframework.web.servlet.mvc.method.annotation.SseEmitter; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.CopyOnWriteArrayList; + +@Service +public class SsePushNotificationService +{ + final List emitters = new CopyOnWriteArrayList<>(); + + public void addEmitter(SseEmitter emitter) + { + emitters.add(emitter); + } + + public void removeEmitter(SseEmitter emitter) + { + emitters.remove(emitter); + } + + // XXX: add arguments, etc... for when to send notifications + public void sendNotification() + { + List deadEmitters = new ArrayList<>(); + + emitters.forEach(emitter -> + { + try + { + emitter.send(SseEmitter.event().data("foo")); // XXX: there are other options... see send() + } + catch (IOException e) + { + deadEmitters.add(emitter); + } + }); + emitters.removeAll(deadEmitters); + } +} diff --git a/app/src/main/java/io/xeres/app/xrs/common/SecurityKey.java b/app/src/main/java/io/xeres/app/xrs/common/SecurityKey.java new file mode 100644 index 000000000..f0b5e2ecc --- /dev/null +++ b/app/src/main/java/io/xeres/app/xrs/common/SecurityKey.java @@ -0,0 +1,89 @@ +/* + * Copyright (c) 2019-2021 by David Gerber - https://zapek.com + * + * This file is part of Xeres. + * + * Xeres is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Xeres is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Xeres. If not, see . + */ + +package io.xeres.app.xrs.common; + +import io.xeres.common.id.GxsId; + +import java.util.EnumSet; +import java.util.Set; + +public class SecurityKey +{ + public enum Flags + { + TYPE_PUBLIC_ONLY, + TYPE_FULL, + UNUSED_3, + UNUSED_4, + DISTRIBUTION_PUBLISH, + DISTRIBUTION_ADMIN, + DISTRIBUTION_IDENTITY; // XXX: not used? actually yes! I got it from the peer after the PUBLISH key (it was an... identity) + + public static Set ofTypes() + { + return EnumSet.of(TYPE_PUBLIC_ONLY, TYPE_FULL); + } + + public static Set ofDistributions() + { + return EnumSet.of(DISTRIBUTION_PUBLISH, DISTRIBUTION_ADMIN, DISTRIBUTION_IDENTITY); + } + } + + private final GxsId gxsId; + private final Set flags; + private final int startTs; + private final int endTs; + private final byte[] data; + + public SecurityKey(GxsId gxsId, Set flags, int startTs, int endTs, byte[] data) + { + this.gxsId = gxsId; + this.flags = flags; + this.startTs = startTs; + this.endTs = endTs; + this.data = data; + } + + public GxsId getGxsId() + { + return gxsId; + } + + public Set getFlags() + { + return flags; + } + + public int getStartTs() + { + return startTs; + } + + public int getEndTs() + { + return endTs; + } + + public byte[] getData() + { + return data; + } +} diff --git a/app/src/main/java/io/xeres/app/xrs/common/SecurityKeySet.java b/app/src/main/java/io/xeres/app/xrs/common/SecurityKeySet.java new file mode 100644 index 000000000..a99cc9758 --- /dev/null +++ b/app/src/main/java/io/xeres/app/xrs/common/SecurityKeySet.java @@ -0,0 +1,67 @@ +/* + * Copyright (c) 2019-2021 by David Gerber - https://zapek.com + * + * This file is part of Xeres. + * + * Xeres is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Xeres is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Xeres. If not, see . + */ + +package io.xeres.app.xrs.common; + +import io.xeres.common.id.GxsId; + +import java.util.HashMap; +import java.util.Map; + +import static io.xeres.app.xrs.common.SecurityKey.Flags.TYPE_FULL; +import static io.xeres.app.xrs.common.SecurityKey.Flags.TYPE_PUBLIC_ONLY; + +public class SecurityKeySet +{ + private final String groupId = ""; // XXX: seems unused, confirm + private final Map privateKeys = new HashMap<>(); + private final Map publicKeys = new HashMap<>(); + + public SecurityKeySet() + { + // Needed + } + + public String getGroupId() + { + return groupId; + } + + public void put(SecurityKey securityKey) + { + if (securityKey.getFlags().contains(TYPE_PUBLIC_ONLY)) + { + publicKeys.put(securityKey.getGxsId(), securityKey); + } + else if (securityKey.getFlags().contains(TYPE_FULL)) + { + privateKeys.put(securityKey.getGxsId(), securityKey); + } + } + + public Map getPrivateKeys() + { + return privateKeys; + } + + public Map getPublicKeys() + { + return publicKeys; + } +} diff --git a/app/src/main/java/io/xeres/app/xrs/common/Signature.java b/app/src/main/java/io/xeres/app/xrs/common/Signature.java new file mode 100644 index 000000000..6fb9b3bb8 --- /dev/null +++ b/app/src/main/java/io/xeres/app/xrs/common/Signature.java @@ -0,0 +1,45 @@ +/* + * Copyright (c) 2019-2021 by David Gerber - https://zapek.com + * + * This file is part of Xeres. + * + * Xeres is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Xeres is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Xeres. If not, see . + */ + +package io.xeres.app.xrs.common; + +import io.xeres.common.id.GxsId; + +// XXX: maybe should be in crypto? not sure... It's related to identities but I don't like the structure +public class Signature +{ + private final GxsId gxsId; + private final byte[] data; + + public Signature(GxsId gxsId, byte[] data) + { + this.gxsId = gxsId; + this.data = data; + } + + public GxsId getGxsId() + { + return gxsId; + } + + public byte[] getData() + { + return data; + } +} diff --git a/app/src/main/java/io/xeres/app/xrs/common/SignatureSet.java b/app/src/main/java/io/xeres/app/xrs/common/SignatureSet.java new file mode 100644 index 000000000..32735dd64 --- /dev/null +++ b/app/src/main/java/io/xeres/app/xrs/common/SignatureSet.java @@ -0,0 +1,68 @@ +/* + * Copyright (c) 2019-2021 by David Gerber - https://zapek.com + * + * This file is part of Xeres. + * + * Xeres is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Xeres is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Xeres. If not, see . + */ + +package io.xeres.app.xrs.common; + +import java.util.Arrays; +import java.util.HashMap; +import java.util.Map; + +public class SignatureSet +{ + public enum Type + { + IDENTITY(0x10), + PUBLISH(0x20), + ADMIN(0x40); + + Type(int value) + { + this.value = value; + } + + private final int value; + + public int getValue() + { + return value; + } + + public static Type findByValue(int value) + { + return Arrays.stream(values()).filter(type -> type.getValue() == value).findFirst().orElseThrow(); + } + } + + private final Map signatures = new HashMap<>(); + + public SignatureSet() + { + + } + + public void put(Type type, Signature signature) + { + signatures.put(type.getValue(), signature); + } + + public Map getSignatures() + { + return signatures; + } +} diff --git a/app/src/main/java/io/xeres/app/xrs/item/Item.java b/app/src/main/java/io/xeres/app/xrs/item/Item.java new file mode 100644 index 000000000..aa506b316 --- /dev/null +++ b/app/src/main/java/io/xeres/app/xrs/item/Item.java @@ -0,0 +1,108 @@ +/* + * Copyright (c) 2019-2021 by David Gerber - https://zapek.com + * + * This file is part of Xeres. + * + * Xeres is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Xeres is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Xeres. If not, see . + */ + +package io.xeres.app.xrs.item; + +import io.netty.buffer.ByteBuf; +import io.netty.buffer.ByteBufAllocator; +import io.netty.util.ReferenceCountUtil; +import io.xeres.app.xrs.serialization.RsSerializable; +import io.xeres.app.xrs.serialization.SerializationFlags; +import io.xeres.app.xrs.serialization.Serializer; +import io.xeres.app.xrs.service.RsService; +import io.xeres.app.xrs.service.RsServiceType; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.Set; + +import static io.xeres.app.net.peer.packet.Packet.HEADER_SIZE; + +public class Item +{ + private static final Logger log = LoggerFactory.getLogger(Item.class); + + protected ByteBuf buf; + private RsService service; + + public Item() + { + // Needed for instantiation + } + + public void setIncoming(ByteBuf buf) + { + this.buf = buf; + } + + public void setOutgoing(ByteBufAllocator allocator, int version, RsServiceType service, int subType) + { + buf = allocator.buffer(); + buf.writeByte(version); + buf.writeShort(service.getType()); + buf.writeByte(subType); + buf.writeInt(HEADER_SIZE); + } + + public RawItem serializeItem(Set flags) + { + var size = 0; + + if (RsSerializable.class.isAssignableFrom(getClass())) + { + log.trace("Serializing class {} using writeObject(), flags: {}", getClass().getSimpleName(), flags); + size = ((RsSerializable) this).writeObject(buf, flags); + } + else + { + log.trace("Serializing class {} using annotations", getClass().getSimpleName()); + size += Serializer.serializeAnnotatedFields(buf, this); + } + log.debug("==> {} ({})", getClass().getSimpleName(), size + HEADER_SIZE); + setItemSize(size + HEADER_SIZE); + + return new RawItem(buf); + } + + public int getPriority() + { + return ItemPriority.DEFAULT.getPriority(); + } + + public void dispose() + { + assert buf.refCnt() == 1; + ReferenceCountUtil.release(buf); + } + + protected void setItemSize(int size) + { + buf.setInt(4, size); + } + + public RsService getService() + { + return service; + } + + public void setService(RsService service) + { + this.service = service; + } +} diff --git a/app/src/main/java/io/xeres/app/xrs/item/ItemFactory.java b/app/src/main/java/io/xeres/app/xrs/item/ItemFactory.java new file mode 100644 index 000000000..4ee8ab070 --- /dev/null +++ b/app/src/main/java/io/xeres/app/xrs/item/ItemFactory.java @@ -0,0 +1,54 @@ +/* + * Copyright (c) 2019-2021 by David Gerber - https://zapek.com + * + * This file is part of Xeres. + * + * Xeres is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Xeres is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Xeres. If not, see . + */ + +package io.xeres.app.xrs.item; + +import io.xeres.app.xrs.service.RsServiceRegistry; + +import java.lang.reflect.InvocationTargetException; + +public final class ItemFactory +{ + private ItemFactory() + { + throw new UnsupportedOperationException("Utility class"); + } + + public static Item create(int type, int subType) + { + var service = RsServiceRegistry.getServiceFromType(type); + if (service != null) + { + try + { + var item = service.createItem(subType); + item.setService(service); + return item; + } + catch (InstantiationException | IllegalAccessException | InvocationTargetException | NoSuchMethodException e) + { + throw new IllegalStateException("Couldn't create item in service " + service + " : " + e.getMessage()); + } + } + else + { + throw new IllegalStateException("Couldn't create item for service " + type + ": no service found"); + } + } +} diff --git a/app/src/main/java/io/xeres/app/xrs/item/ItemPriority.java b/app/src/main/java/io/xeres/app/xrs/item/ItemPriority.java new file mode 100644 index 000000000..c40b571af --- /dev/null +++ b/app/src/main/java/io/xeres/app/xrs/item/ItemPriority.java @@ -0,0 +1,22 @@ +package io.xeres.app.xrs.item; + +public enum ItemPriority +{ + DEFAULT(3), + GXS(6), + CHAT(7); + // XXX: add other priorities here, find good names. does RTT really have default?, etc... + + private final int priority; + + ItemPriority(int priority) + { + this.priority = priority; + } + + public int getPriority() + { + return priority; + } +} + diff --git a/app/src/main/java/io/xeres/app/xrs/item/RawItem.java b/app/src/main/java/io/xeres/app/xrs/item/RawItem.java new file mode 100644 index 000000000..dd3d93dba --- /dev/null +++ b/app/src/main/java/io/xeres/app/xrs/item/RawItem.java @@ -0,0 +1,143 @@ +/* + * Copyright (c) 2019-2021 by David Gerber - https://zapek.com + * + * This file is part of Xeres. + * + * Xeres is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Xeres is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Xeres. If not, see . + */ + +package io.xeres.app.xrs.item; + +import io.netty.buffer.ByteBuf; +import io.netty.util.ReferenceCountUtil; +import io.xeres.app.net.peer.packet.Packet; +import io.xeres.app.xrs.serialization.RsSerializable; +import io.xeres.app.xrs.serialization.SerializationFlags; +import io.xeres.app.xrs.serialization.Serializer; +import org.bouncycastle.util.encoders.Hex; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.EnumSet; + +import static io.xeres.app.net.peer.packet.Packet.HEADER_SIZE; + +public class RawItem +{ + private static final Logger log = LoggerFactory.getLogger(RawItem.class); + + private int priority = 3; // XXX: priority levels, implement later (see itempriorities.h) + protected ByteBuf buf; + + public RawItem() + { + } + + public RawItem(Packet packet) + { + priority = packet.getPriority(); + buf = packet.getItemBuffer(); + } + + public RawItem(ByteBuf buf) + { + this.buf = buf; + } + + public Item deserialize() + { + var item = buildItem(); + item.setIncoming(buf); + + buf.skipBytes(HEADER_SIZE); + // XXX: oh, also also see if the size matches the total size + if (RsSerializable.class.isAssignableFrom(item.getClass())) + { + // If the object implements RsSerializable which is more flexible, use it + log.trace("Deserializing class {} using readObject()", item.getClass().getSimpleName()); + ((RsSerializable) item).readObject(buf, EnumSet.noneOf(SerializationFlags.class)); + } + else + { + // Otherwise use the more convenient @RsSerialized notations (recommended) + log.trace("Deserializing class {} using annotations", item.getClass().getSimpleName()); + Serializer.deserializeAnnotatedFields(buf, item); + } + log.debug("<== {}", item); + return item; + } + + private Item buildItem() + { + switch (getPacketVersion()) + { + case 1 -> log.warn("Packet version 1 not supported yet"); + case 2 -> { + return ItemFactory.create(getPacketService(), getPacketSubType()); + } + default -> log.warn("Packet version {} not supported", getPacketVersion()); + } + return new Item(); // will just get disposed + } + + private int getPacketVersion() + { + return buf.getUnsignedByte(0); + } + + private int getPacketService() + { + return buf.getUnsignedShort(1); + } + + private int getPacketSubType() + { + return buf.getUnsignedByte(3); + } + + public ByteBuf getBuffer() + { + return buf; + } + + public int getPriority() + { + return priority; + } + + public void dispose() + { + ReferenceCountUtil.release(buf); + } + + @Override + public String toString() + { + String bufOut = null; + if (buf != null) + { + buf.markReaderIndex(); + buf.readerIndex(0); + var out = new byte[buf.writerIndex()]; + buf.readBytes(out); + buf.resetReaderIndex(); + bufOut = new String(Hex.encode(out)); + } + + return "RawItem{" + + "priority=" + priority + + ", buf=" + bufOut + + '}'; + } +} diff --git a/app/src/main/java/io/xeres/app/xrs/serialization/AnnotationSerializer.java b/app/src/main/java/io/xeres/app/xrs/serialization/AnnotationSerializer.java new file mode 100644 index 000000000..ed51b6342 --- /dev/null +++ b/app/src/main/java/io/xeres/app/xrs/serialization/AnnotationSerializer.java @@ -0,0 +1,109 @@ +/* + * Copyright (c) 2019-2021 by David Gerber - https://zapek.com + * + * This file is part of Xeres. + * + * Xeres is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Xeres is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Xeres. If not, see . + */ + +package io.xeres.app.xrs.serialization; + +import io.netty.buffer.ByteBuf; +import io.xeres.app.xrs.item.Item; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.lang.reflect.Field; +import java.lang.reflect.InvocationTargetException; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; + +final class AnnotationSerializer +{ + private static final Logger log = LoggerFactory.getLogger(AnnotationSerializer.class); + + private AnnotationSerializer() + { + throw new UnsupportedOperationException("Utility class"); + } + + static int serialize(ByteBuf buf, Object object) + { + var size = 0; + + for (Field field : getAllFields(object.getClass())) + { + log.trace("Serializing field {}, of type {}", field.getName(), field.getType().getSimpleName()); + size += Serializer.serialize(buf, field, object, field.getAnnotation(RsSerialized.class)); + } + return size; + } + + static Object deserialize(ByteBuf buf, Class javaClass) + { + Object instanceObject; + try + { + instanceObject = javaClass.getDeclaredConstructor().newInstance(); + } + catch (InstantiationException | IllegalAccessException | InvocationTargetException | NoSuchMethodException e) + { + throw new IllegalArgumentException("Cannot instantiate object of class " + javaClass.getSimpleName()); + } + if (!deserialize(buf, instanceObject)) + { + throw new IllegalArgumentException("Cannot deserialize object of class " + javaClass.getSimpleName()); + } + return instanceObject; + } + + static boolean deserialize(ByteBuf buf, Object object) + { + List allFields = getAllFields(object.getClass()); + + for (Field field : allFields) + { + log.trace("Deserializing field {}, of type {}", field.getName(), field.getType().getSimpleName()); + Serializer.deserialize(buf, field, object, field.getAnnotation(RsSerialized.class)); + } + return !allFields.isEmpty(); + } + + /** + * Search all fields annotated with @RsSerialized, starting with the + * first subclass of Item down to the last subclass.
+ * + * @param javaClass the class + * @return all fields ordered from superclass to subclass + */ + private static List getAllFields(Class javaClass) + { + if (javaClass == null || javaClass == Item.class) + { + return Collections.emptyList(); + } + + List superFields = new ArrayList<>(getAllFields(javaClass.getSuperclass())); + List classFields = Arrays.stream(javaClass.getDeclaredFields()) + .filter(field -> { + field.setAccessible(true); // NOSONAR + return field.isAnnotationPresent(RsSerialized.class); + }) + .toList(); + superFields.addAll(classFields); + return superFields; + } +} diff --git a/app/src/main/java/io/xeres/app/xrs/serialization/ArraySerializer.java b/app/src/main/java/io/xeres/app/xrs/serialization/ArraySerializer.java new file mode 100644 index 000000000..5ce5ad18f --- /dev/null +++ b/app/src/main/java/io/xeres/app/xrs/serialization/ArraySerializer.java @@ -0,0 +1,54 @@ +/* + * Copyright (c) 2019-2021 by David Gerber - https://zapek.com + * + * This file is part of Xeres. + * + * Xeres is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Xeres is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Xeres. If not, see . + */ + +package io.xeres.app.xrs.serialization; + +import io.netty.buffer.ByteBuf; + +final class ArraySerializer +{ + private ArraySerializer() + { + throw new UnsupportedOperationException("Utility class"); + } + + static int serialize(ByteBuf buf, Class javaClass, Object object) + { + if (javaClass.equals(byte[].class)) + { + return ByteArraySerializer.serialize(buf, (byte[]) object); + } + else + { + throw new IllegalArgumentException("Unhandled array type " + javaClass.getSimpleName()); // XXX: handle other types (see what RS uses...) + } + } + + static Object deserialize(ByteBuf buf, Class javaClass) + { + if (javaClass.equals(byte[].class)) + { + return ByteArraySerializer.deserialize(buf); + } + else + { + throw new IllegalArgumentException("Unhandled array type " + javaClass.getSimpleName()); // XXX + } + } +} diff --git a/app/src/main/java/io/xeres/app/xrs/serialization/BooleanSerializer.java b/app/src/main/java/io/xeres/app/xrs/serialization/BooleanSerializer.java new file mode 100644 index 000000000..a9e708f6b --- /dev/null +++ b/app/src/main/java/io/xeres/app/xrs/serialization/BooleanSerializer.java @@ -0,0 +1,52 @@ +/* + * Copyright (c) 2019-2021 by David Gerber - https://zapek.com + * + * This file is part of Xeres. + * + * Xeres is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Xeres is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Xeres. If not, see . + */ + +package io.xeres.app.xrs.serialization; + +import io.netty.buffer.ByteBuf; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.Objects; + +final class BooleanSerializer +{ + private static final Logger log = LoggerFactory.getLogger(BooleanSerializer.class); + + private BooleanSerializer() + { + throw new UnsupportedOperationException("Utility class"); + } + + static int serialize(ByteBuf buf, Boolean bool) + { + Objects.requireNonNull(bool, "Null boolean not supported"); + log.trace("Writing boolean: {}", bool); + buf.ensureWritable(1); + buf.writeBoolean(bool); + return 1; + } + + static boolean deserialize(ByteBuf buf) + { + var val = buf.readBoolean(); + log.trace("Reading boolean: {}", val); + return val; + } +} diff --git a/app/src/main/java/io/xeres/app/xrs/serialization/ByteArraySerializer.java b/app/src/main/java/io/xeres/app/xrs/serialization/ByteArraySerializer.java new file mode 100644 index 000000000..343591d09 --- /dev/null +++ b/app/src/main/java/io/xeres/app/xrs/serialization/ByteArraySerializer.java @@ -0,0 +1,74 @@ +/* + * Copyright (c) 2019-2021 by David Gerber - https://zapek.com + * + * This file is part of Xeres. + * + * Xeres is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Xeres is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Xeres. If not, see . + */ + +package io.xeres.app.xrs.serialization; + +import io.netty.buffer.ByteBuf; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +final class ByteArraySerializer +{ + private static final Logger log = LoggerFactory.getLogger(ByteArraySerializer.class); + + private ByteArraySerializer() + { + throw new UnsupportedOperationException("Utility class"); + } + + static int serialize(ByteBuf buf, byte[] a) + { + if (a == null) + { + buf.ensureWritable(4); + buf.writeInt(0); + return 4; + } + log.trace("Writing byte array of size {}", a.length); + buf.ensureWritable(4 + a.length); + buf.writeInt(a.length); + buf.writeBytes(a); + return 4 + a.length; + } + + static byte[] deserialize(ByteBuf buf) + { + var len = buf.readInt(); + log.trace("Reading byte array of size {}", len); + var out = new byte[len]; + buf.readBytes(out); + return out; + } + + static int serialize(ByteBuf buf, byte[] array, int size) + { + log.trace("Writing byte array of size {}", size); + buf.ensureWritable(size); + buf.writeBytes(array, 0, size); + return size; + } + + static byte[] deserialize(ByteBuf buf, int size) + { + log.trace("Reading byte array of size {}", size); + var out = new byte[size]; + buf.readBytes(out); + return out; + } +} diff --git a/app/src/main/java/io/xeres/app/xrs/serialization/ByteSerializer.java b/app/src/main/java/io/xeres/app/xrs/serialization/ByteSerializer.java new file mode 100644 index 000000000..b1b448039 --- /dev/null +++ b/app/src/main/java/io/xeres/app/xrs/serialization/ByteSerializer.java @@ -0,0 +1,52 @@ +/* + * Copyright (c) 2019-2021 by David Gerber - https://zapek.com + * + * This file is part of Xeres. + * + * Xeres is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Xeres is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Xeres. If not, see . + */ + +package io.xeres.app.xrs.serialization; + +import io.netty.buffer.ByteBuf; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.Objects; + +final class ByteSerializer +{ + private static final Logger log = LoggerFactory.getLogger(ByteSerializer.class); + + private ByteSerializer() + { + throw new UnsupportedOperationException("Utility class"); + } + + static int serialize(ByteBuf buf, Byte b) + { + Objects.requireNonNull(b, "Null byte not supported"); + log.trace("Writing byte: {}", b); + buf.ensureWritable(1); + buf.writeByte(b); + return 1; + } + + static byte deserialize(ByteBuf buf) + { + var val = buf.readByte(); + log.trace("Reading byte: {}", val); + return val; + } +} diff --git a/app/src/main/java/io/xeres/app/xrs/serialization/DoubleSerializer.java b/app/src/main/java/io/xeres/app/xrs/serialization/DoubleSerializer.java new file mode 100644 index 000000000..6d8f058f6 --- /dev/null +++ b/app/src/main/java/io/xeres/app/xrs/serialization/DoubleSerializer.java @@ -0,0 +1,52 @@ +/* + * Copyright (c) 2019-2021 by David Gerber - https://zapek.com + * + * This file is part of Xeres. + * + * Xeres is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Xeres is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Xeres. If not, see . + */ + +package io.xeres.app.xrs.serialization; + +import io.netty.buffer.ByteBuf; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.Objects; + +final class DoubleSerializer +{ + private static final Logger log = LoggerFactory.getLogger(DoubleSerializer.class); + + private DoubleSerializer() + { + throw new UnsupportedOperationException("Utility class"); + } + + static int serialize(ByteBuf buf, Double d) + { + Objects.requireNonNull(d, "Null double not supported"); + log.trace("Writing double: {}", d); + buf.ensureWritable(8); + buf.writeDouble(d); + return 8; + } + + static double deserialize(ByteBuf buf) + { + var val = buf.readDouble(); + log.debug("Reading double: {}", val); + return val; + } +} diff --git a/app/src/main/java/io/xeres/app/xrs/serialization/EnumSerializer.java b/app/src/main/java/io/xeres/app/xrs/serialization/EnumSerializer.java new file mode 100644 index 000000000..580e8872f --- /dev/null +++ b/app/src/main/java/io/xeres/app/xrs/serialization/EnumSerializer.java @@ -0,0 +1,53 @@ +/* + * Copyright (c) 2019-2021 by David Gerber - https://zapek.com + * + * This file is part of Xeres. + * + * Xeres is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Xeres is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Xeres. If not, see . + */ + +package io.xeres.app.xrs.serialization; + +import io.netty.buffer.ByteBuf; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.Objects; + +final class EnumSerializer +{ + private static final Logger log = LoggerFactory.getLogger(EnumSerializer.class); + + private EnumSerializer() + { + throw new UnsupportedOperationException("Utility class"); + } + + static int serialize(ByteBuf buf, Enum> e) + { + Objects.requireNonNull(e, "Null enum not supported"); + log.trace("Writing enum: {}", e.ordinal()); + buf.ensureWritable(4); + buf.writeInt(e.ordinal()); + return 4; + } + + @SuppressWarnings("unchecked") + static > E deserialize(ByteBuf buf, Class e) + { + var val = buf.readInt(); + log.trace("Reading enum: {}, class: {}", val, e.getSimpleName()); + return (E) e.getEnumConstants()[val]; + } +} diff --git a/app/src/main/java/io/xeres/app/xrs/serialization/EnumSetSerializer.java b/app/src/main/java/io/xeres/app/xrs/serialization/EnumSetSerializer.java new file mode 100644 index 000000000..6b10a6287 --- /dev/null +++ b/app/src/main/java/io/xeres/app/xrs/serialization/EnumSetSerializer.java @@ -0,0 +1,181 @@ +/* + * Copyright (c) 2019-2021 by David Gerber - https://zapek.com + * + * This file is part of Xeres. + * + * Xeres is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Xeres is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Xeres. If not, see . + */ + +package io.xeres.app.xrs.serialization; + +import io.netty.buffer.ByteBuf; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.lang.reflect.ParameterizedType; +import java.util.EnumSet; +import java.util.Objects; +import java.util.Set; + +final class EnumSetSerializer +{ + private static final Logger log = LoggerFactory.getLogger(EnumSetSerializer.class); + + private EnumSetSerializer() + { + throw new UnsupportedOperationException("Utility class"); + } + + static int serialize(ByteBuf buf, Set> enumSet, RsSerialized annotation) + { + Objects.requireNonNull(enumSet, "Null enumset not supported"); + Objects.requireNonNull(annotation, "Annotation is needed for EnumSet"); + var fieldSize = annotation.fieldSize(); + + return serialize(buf, enumSet, fieldSize); + } + + static int serialize(ByteBuf buf, Set> enumSet, FieldSize fieldSize) + { + return switch (fieldSize) + { + case INTEGER -> serializeEnumSetInt(buf, enumSet); + case BYTE -> serializeEnumSetByte(buf, enumSet); + case SHORT -> serializeEnumSetShort(buf, enumSet); + }; + } + + private static int serializeEnumSetInt(ByteBuf buf, Set> enumSet) + { + if (enumSet.size() > 32) + { + throw new IllegalArgumentException("EnumSet cannot have more than 32 entries"); + } + var size = 4; + + log.trace("Enumset (int): {}", enumSet); + buf.ensureWritable(size); + var value = 0; + for (Enum anEnum : enumSet) + { + value |= 1 << anEnum.ordinal(); + } + buf.writeInt(value); + return size; + } + + private static int serializeEnumSetByte(ByteBuf buf, Set> enumSet) + { + if (enumSet.size() > 8) + { + throw new IllegalArgumentException("EnumSet for a byte cannot have more than 8 entries"); + } + var size = 1; + + log.trace("Enumset (byte): {}", enumSet); + buf.ensureWritable(size); + byte value = 0; + for (Enum anEnum : enumSet) + { + value |= 1 << anEnum.ordinal(); + } + buf.writeByte(value); + return size; + } + + private static int serializeEnumSetShort(ByteBuf buf, Set> enumSet) + { + if (enumSet.size() > 16) + { + throw new IllegalArgumentException("EnumSet for a short cannot have more than 16 entries"); + } + var size = 2; + + log.trace("Enumset (short): {}", enumSet); + buf.ensureWritable(size); + short value = 0; + for (Enum anEnum : enumSet) + { + value |= 1 << anEnum.ordinal(); + } + buf.writeShort(value); + return size; + } + + @SuppressWarnings("unchecked") + static > Set deserialize(ByteBuf buf, ParameterizedType type, RsSerialized annotation) + { + Objects.requireNonNull(annotation, "Annotation is needed for EnumSet"); + Class enumClass = (Class) type.getActualTypeArguments()[0]; + + var fieldSize = annotation.fieldSize(); + + return deserialize(buf, enumClass, fieldSize); + } + + static > Set deserialize(ByteBuf buf, Class e, FieldSize fieldSize) + { + return switch (fieldSize) + { + case INTEGER -> deserializeEnumSetInt(buf, e); + case BYTE -> deserializeEnumSetByte(buf, e); + case SHORT -> deserializeEnumSetShort(buf, e); + }; + } + + private static > Set deserializeEnumSetInt(ByteBuf buf, Class e) + { + var value = buf.readInt(); + log.trace("Reading enumSet (int): {}", value); + var enumSet = EnumSet.noneOf(e); + for (E enumConstant : e.getEnumConstants()) + { + if ((value & (1 << enumConstant.ordinal())) != 0) + { + enumSet.add(enumConstant); + } + } + return enumSet; + } + + private static > Set deserializeEnumSetByte(ByteBuf buf, Class e) + { + var value = buf.readByte(); + log.trace("Reading enumSet (byte): {}", value); + var enumSet = EnumSet.noneOf(e); + for (E enumConstant : e.getEnumConstants()) + { + if ((value & 0xff & (1 << enumConstant.ordinal())) != 0) + { + enumSet.add(enumConstant); + } + } + return enumSet; + } + + private static > Set deserializeEnumSetShort(ByteBuf buf, Class e) + { + var value = buf.readShort(); + log.trace("Reading enumSet (long): {}", value); + var enumSet = EnumSet.noneOf(e); + for (E enumConstant : e.getEnumConstants()) + { + if ((value & (1 << enumConstant.ordinal())) != 0) + { + enumSet.add(enumConstant); + } + } + return enumSet; + } +} diff --git a/app/src/main/java/io/xeres/app/xrs/serialization/FieldSize.java b/app/src/main/java/io/xeres/app/xrs/serialization/FieldSize.java new file mode 100644 index 000000000..09ad07aae --- /dev/null +++ b/app/src/main/java/io/xeres/app/xrs/serialization/FieldSize.java @@ -0,0 +1,27 @@ +/* + * Copyright (c) 2019-2021 by David Gerber - https://zapek.com + * + * This file is part of Xeres. + * + * Xeres is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Xeres is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Xeres. If not, see . + */ + +package io.xeres.app.xrs.serialization; + +public enum FieldSize +{ + BYTE, + SHORT, + INTEGER +} diff --git a/app/src/main/java/io/xeres/app/xrs/serialization/FloatSerializer.java b/app/src/main/java/io/xeres/app/xrs/serialization/FloatSerializer.java new file mode 100644 index 000000000..770084d4d --- /dev/null +++ b/app/src/main/java/io/xeres/app/xrs/serialization/FloatSerializer.java @@ -0,0 +1,52 @@ +/* + * Copyright (c) 2019-2021 by David Gerber - https://zapek.com + * + * This file is part of Xeres. + * + * Xeres is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Xeres is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Xeres. If not, see . + */ + +package io.xeres.app.xrs.serialization; + +import io.netty.buffer.ByteBuf; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.Objects; + +final class FloatSerializer +{ + private static final Logger log = LoggerFactory.getLogger(FloatSerializer.class); + + private FloatSerializer() + { + throw new UnsupportedOperationException("Utility class"); + } + + static int serialize(ByteBuf buf, Float f) + { + Objects.requireNonNull(f, "Null float not supported"); + log.trace("Writing float: {}", f); + buf.ensureWritable(4); + buf.writeFloat(f); + return 4; + } + + static float deserialize(ByteBuf buf) + { + var val = buf.readFloat(); + log.trace("Reading float: {}", val); + return val; + } +} diff --git a/app/src/main/java/io/xeres/app/xrs/serialization/IdentifierSerializer.java b/app/src/main/java/io/xeres/app/xrs/serialization/IdentifierSerializer.java new file mode 100644 index 000000000..3aa78ab1b --- /dev/null +++ b/app/src/main/java/io/xeres/app/xrs/serialization/IdentifierSerializer.java @@ -0,0 +1,96 @@ +/* + * Copyright (c) 2019-2021 by David Gerber - https://zapek.com + * + * This file is part of Xeres. + * + * Xeres is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Xeres is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Xeres. If not, see . + */ + +package io.xeres.app.xrs.serialization; + +import io.netty.buffer.ByteBuf; +import io.xeres.common.id.Identifier; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Modifier; +import java.util.Arrays; + +final class IdentifierSerializer +{ + private static final Logger log = LoggerFactory.getLogger(IdentifierSerializer.class); + + private IdentifierSerializer() + { + throw new UnsupportedOperationException("Utility class"); + } + + static int serialize(ByteBuf buf, Class identifierClass, Identifier identifier) + { + log.trace("Writing identifier: {}", identifier); + if (identifier == null) + { + try + { + identifier = (Identifier) identifierClass.getDeclaredConstructor().newInstance(); + buf.ensureWritable(identifier.getLength()); + buf.writeBytes(identifier.getNullIdentifier()); + } + catch (InstantiationException | IllegalAccessException | InvocationTargetException | NoSuchMethodException e) + { + throw new IllegalStateException(e.getMessage()); + } + } + else + { + buf.ensureWritable(identifier.getLength()); + buf.writeBytes(identifier.getBytes()); + } + return identifier.getLength(); + } + + static Identifier deserialize(ByteBuf buf, Class identifierClass) + { + try + { + //noinspection PrimitiveArrayArgumentToVarargsMethod + Identifier identifier = (Identifier) identifierClass.getDeclaredConstructor(byte[].class).newInstance(ByteArraySerializer.deserialize(buf, getIdentifierLength(identifierClass))); + if (Arrays.equals(identifier.getNullIdentifier(), identifier.getBytes())) + { + return null; + } + return identifier; + } + catch (InstantiationException | IllegalAccessException | InvocationTargetException | NoSuchMethodException e) + { + throw new IllegalStateException(e.getMessage()); + } + } + + private static int getIdentifierLength(Class identifierClass) + { + try + { + return (int) Arrays.stream(identifierClass.getDeclaredFields()) + .filter(field -> Modifier.isStatic(field.getModifiers()) && field.getName().equals("LENGTH")) + .findFirst().orElseThrow(() -> new IllegalArgumentException("Missing LENGTH static field in " + identifierClass.getSimpleName())) + .get(null); + } + catch (IllegalAccessException e) + { + throw new IllegalStateException(e.getMessage()); + } + } +} diff --git a/app/src/main/java/io/xeres/app/xrs/serialization/IntSerializer.java b/app/src/main/java/io/xeres/app/xrs/serialization/IntSerializer.java new file mode 100644 index 000000000..e0927743b --- /dev/null +++ b/app/src/main/java/io/xeres/app/xrs/serialization/IntSerializer.java @@ -0,0 +1,52 @@ +/* + * Copyright (c) 2019-2021 by David Gerber - https://zapek.com + * + * This file is part of Xeres. + * + * Xeres is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Xeres is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Xeres. If not, see . + */ + +package io.xeres.app.xrs.serialization; + +import io.netty.buffer.ByteBuf; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.Objects; + +final class IntSerializer +{ + private static final Logger log = LoggerFactory.getLogger(IntSerializer.class); + + private IntSerializer() + { + throw new UnsupportedOperationException("Utility class"); + } + + static int serialize(ByteBuf buf, Integer i) + { + Objects.requireNonNull(i, "Null integer not supported"); + log.trace("Writing int: {}", i); + buf.ensureWritable(4); + buf.writeInt(i); + return 4; + } + + static int deserialize(ByteBuf buf) + { + var val = buf.readInt(); + log.trace("Reading int: {}", val); + return val; + } +} diff --git a/app/src/main/java/io/xeres/app/xrs/serialization/ListSerializer.java b/app/src/main/java/io/xeres/app/xrs/serialization/ListSerializer.java new file mode 100644 index 000000000..49f1f1c04 --- /dev/null +++ b/app/src/main/java/io/xeres/app/xrs/serialization/ListSerializer.java @@ -0,0 +1,79 @@ +/* + * Copyright (c) 2019-2021 by David Gerber - https://zapek.com + * + * This file is part of Xeres. + * + * Xeres is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Xeres is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Xeres. If not, see . + */ + +package io.xeres.app.xrs.serialization; + +import io.netty.buffer.ByteBuf; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.lang.reflect.ParameterizedType; +import java.util.ArrayList; +import java.util.List; + +final class ListSerializer +{ + private static final Logger log = LoggerFactory.getLogger(ListSerializer.class); + + private ListSerializer() + { + throw new UnsupportedOperationException("Utility class"); + } + + static int serialize(ByteBuf buf, List list) + { + var size = 4; + + buf.ensureWritable(4); + if (list != null) + { + log.trace("Entries in List: {}", list.size()); + buf.writeInt(list.size()); + for (Object data : list) + { + size += Serializer.serialize(buf, data.getClass(), data, null); + } + } + else + { + buf.writeInt(0); + } + return size; + } + + static List deserialize(ByteBuf buf, List list, ParameterizedType type) + { + if (list == null) + { + list = new ArrayList<>(); + } + + var entries = buf.readInt(); + Class dataClass = (Class) type.getActualTypeArguments()[0]; + log.trace("Data class: {}", dataClass.getSimpleName()); + + while (entries-- > 0) + { + var dataObject = Serializer.deserialize(buf, dataClass); + log.trace("result: {}", dataObject); + list.add(dataObject); + } + return list; + } +} diff --git a/app/src/main/java/io/xeres/app/xrs/serialization/LongSerializer.java b/app/src/main/java/io/xeres/app/xrs/serialization/LongSerializer.java new file mode 100644 index 000000000..2fd151d91 --- /dev/null +++ b/app/src/main/java/io/xeres/app/xrs/serialization/LongSerializer.java @@ -0,0 +1,52 @@ +/* + * Copyright (c) 2019-2021 by David Gerber - https://zapek.com + * + * This file is part of Xeres. + * + * Xeres is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Xeres is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Xeres. If not, see . + */ + +package io.xeres.app.xrs.serialization; + +import io.netty.buffer.ByteBuf; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.Objects; + +final class LongSerializer +{ + private static final Logger log = LoggerFactory.getLogger(LongSerializer.class); + + private LongSerializer() + { + throw new UnsupportedOperationException("Utility class"); + } + + static int serialize(ByteBuf buf, Long l) + { + Objects.requireNonNull(l, "Null long not supported"); + log.trace("Writing long: {}", l); + buf.ensureWritable(8); + buf.writeLong(l); + return 8; + } + + static long deserialize(ByteBuf buf) + { + var val = buf.readLong(); + log.trace("Reading long: {}", val); + return val; + } +} diff --git a/app/src/main/java/io/xeres/app/xrs/serialization/MapSerializer.java b/app/src/main/java/io/xeres/app/xrs/serialization/MapSerializer.java new file mode 100644 index 000000000..b97ab8c55 --- /dev/null +++ b/app/src/main/java/io/xeres/app/xrs/serialization/MapSerializer.java @@ -0,0 +1,144 @@ +/* + * Copyright (c) 2019-2021 by David Gerber - https://zapek.com + * + * This file is part of Xeres. + * + * Xeres is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Xeres is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Xeres. If not, see . + */ + +package io.xeres.app.xrs.serialization; + +import io.netty.buffer.ByteBuf; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.lang.reflect.ParameterizedType; +import java.util.HashMap; +import java.util.Map; + +import static io.xeres.app.xrs.serialization.Serializer.TLV_HEADER_SIZE; + +final class MapSerializer +{ + private static final Logger log = LoggerFactory.getLogger(MapSerializer.class); + + private MapSerializer() + { + throw new UnsupportedOperationException("Utility class"); + } + + static int serialize(ByteBuf buf, Map map) + { + var size = 0; + + if (map != null && map.size() > 0) + { + log.trace("Entries in Map: {}", map.size()); + var mapSize = 0; + int mapSizeOffset = writeTlv(buf); + for (Map.Entry entry : map.entrySet()) + { + int entrySizeOffset = writeTlv(buf); + var entrySize = 0; + log.trace("Key class: {}", entry.getKey().getClass().getSimpleName()); + entrySize += writeMapData(buf, entry.getKey()); + log.trace("Value class: {}", entry.getValue().getClass().getSimpleName()); + entrySize += writeMapData(buf, entry.getValue()); + mapSize += writeTlvBack(buf, entrySizeOffset, entrySize); + log.trace("Writing total entry size of {}", entrySize); + } + log.trace("Writing total map size of {}", mapSize); + size += writeTlvBack(buf, mapSizeOffset, mapSize); + } + else + { + size += writeTlvBack(buf, writeTlv(buf), 0); + } + return size; + } + + static Map deserialize(ByteBuf buf, Map map, ParameterizedType type) + { + if (map == null) + { + map = new HashMap<>(); + } + + int mapSize = readTlv(buf); + log.trace("Map size: {}, readerIndex: {}", mapSize, buf.readerIndex()); + int mapIndex = buf.readerIndex(); + + while (buf.readerIndex() < mapIndex + mapSize - TLV_HEADER_SIZE) + { + log.trace("buf.readerIndex: {}, mapIndex + mapSize: {}", buf.readerIndex(), mapIndex + mapSize); + readTlv(buf); + + Class keyClass = (Class) type.getActualTypeArguments()[0]; + log.trace("Key class: {}", keyClass.getSimpleName()); + var keyObject = readMapData(buf, keyClass); + Class dataClass = (Class) type.getActualTypeArguments()[1]; + log.trace("Data class: {}", dataClass.getSimpleName()); + var dataObject = readMapData(buf, dataClass); + log.trace("result: {}", dataObject); + + map.put(keyObject, dataObject); + } + log.trace("done: buf.readerIndex: {}", buf.readerIndex()); + return map; + } + + private static int writeMapData(ByteBuf buf, Object object) + { + int size; + + int sizeOffset = writeTlv(buf); + size = Serializer.serialize(buf, object.getClass(), object, null); + return writeTlvBack(buf, sizeOffset, size); + } + + // XXX: we don't really need to check for the sizes everywhere. first deserialize can check the total size, then the rest just locally. just throw something if deserializing is wrong + + private static int writeTlvBack(ByteBuf buf, int offset, int size) + { + size += TLV_HEADER_SIZE; + buf.setInt(offset, size); + return size; + } + + private static int writeTlv(ByteBuf buf) + { + buf.ensureWritable(TLV_HEADER_SIZE); + buf.writeShort(1); + int offset = buf.writerIndex(); + buf.writerIndex(offset + 4); + return offset; + } + + private static Object readMapData(ByteBuf buf, Class javaClass) + { + int size = readTlv(buf); // XXX: check size + log.trace("Reading map data of size: {}", size); + + return Serializer.deserialize(buf, javaClass); + } + + private static int readTlv(ByteBuf buf) + { + if (buf.readShort() != 1) + { + throw new IllegalArgumentException("Wrong TLV"); + } + return buf.readInt(); + } +} diff --git a/app/src/main/java/io/xeres/app/xrs/serialization/RsSerializable.java b/app/src/main/java/io/xeres/app/xrs/serialization/RsSerializable.java new file mode 100644 index 000000000..f6cfa4fb5 --- /dev/null +++ b/app/src/main/java/io/xeres/app/xrs/serialization/RsSerializable.java @@ -0,0 +1,31 @@ +/* + * Copyright (c) 2019-2021 by David Gerber - https://zapek.com + * + * This file is part of Xeres. + * + * Xeres is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Xeres is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Xeres. If not, see . + */ + +package io.xeres.app.xrs.serialization; + +import io.netty.buffer.ByteBuf; + +import java.util.Set; + +public interface RsSerializable +{ + int writeObject(ByteBuf buf, Set serializationFlags); + + void readObject(ByteBuf buf, Set serializationFlags); +} diff --git a/app/src/main/java/io/xeres/app/xrs/serialization/RsSerializableSerializer.java b/app/src/main/java/io/xeres/app/xrs/serialization/RsSerializableSerializer.java new file mode 100644 index 000000000..fba509695 --- /dev/null +++ b/app/src/main/java/io/xeres/app/xrs/serialization/RsSerializableSerializer.java @@ -0,0 +1,52 @@ +/* + * Copyright (c) 2019-2021 by David Gerber - https://zapek.com + * + * This file is part of Xeres. + * + * Xeres is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Xeres is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Xeres. If not, see . + */ + +package io.xeres.app.xrs.serialization; + +import io.netty.buffer.ByteBuf; + +import java.lang.reflect.InvocationTargetException; +import java.util.EnumSet; + +final class RsSerializableSerializer +{ + private RsSerializableSerializer() + { + throw new UnsupportedOperationException("Utility class"); + } + + static int serialize(ByteBuf buf, RsSerializable serializable) + { + return serializable.writeObject(buf, EnumSet.noneOf(SerializationFlags.class)); + } + + static Object deserialize(ByteBuf buf, Class javaClass) + { + try + { + Object instanceObject = javaClass.getDeclaredConstructor().newInstance(); + ((RsSerializable) instanceObject).readObject(buf, EnumSet.noneOf(SerializationFlags.class)); + return instanceObject; + } + catch (InstantiationException | IllegalAccessException | NoSuchMethodException | InvocationTargetException e) + { + throw new IllegalStateException("Unhandled class " + javaClass.getSimpleName()); + } + } +} diff --git a/app/src/main/java/io/xeres/app/xrs/serialization/RsSerialized.java b/app/src/main/java/io/xeres/app/xrs/serialization/RsSerialized.java new file mode 100644 index 000000000..117b7b06f --- /dev/null +++ b/app/src/main/java/io/xeres/app/xrs/serialization/RsSerialized.java @@ -0,0 +1,35 @@ +/* + * Copyright (c) 2019-2021 by David Gerber - https://zapek.com + * + * This file is part of Xeres. + * + * Xeres is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Xeres is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Xeres. If not, see . + */ + +package io.xeres.app.xrs.serialization; + +import java.lang.annotation.Retention; +import java.lang.annotation.Target; + +import static java.lang.annotation.ElementType.FIELD; +import static java.lang.annotation.RetentionPolicy.RUNTIME; + +@Retention(RUNTIME) +@Target(FIELD) +public @interface RsSerialized +{ + TlvType tlvType() default TlvType.NONE; + + FieldSize fieldSize() default FieldSize.INTEGER; +} diff --git a/app/src/main/java/io/xeres/app/xrs/serialization/SerializationFlags.java b/app/src/main/java/io/xeres/app/xrs/serialization/SerializationFlags.java new file mode 100644 index 000000000..f98063c1b --- /dev/null +++ b/app/src/main/java/io/xeres/app/xrs/serialization/SerializationFlags.java @@ -0,0 +1,27 @@ +/* + * Copyright (c) 2019-2021 by David Gerber - https://zapek.com + * + * This file is part of Xeres. + * + * Xeres is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Xeres is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Xeres. If not, see . + */ + +package io.xeres.app.xrs.serialization; + +public enum SerializationFlags +{ + SIGNATURE, + SUPERCLASS_ONLY, + SUBCLASS_ONLY +} diff --git a/app/src/main/java/io/xeres/app/xrs/serialization/Serializer.java b/app/src/main/java/io/xeres/app/xrs/serialization/Serializer.java new file mode 100644 index 000000000..8aea7cfe9 --- /dev/null +++ b/app/src/main/java/io/xeres/app/xrs/serialization/Serializer.java @@ -0,0 +1,666 @@ +/* + * Copyright (c) 2019-2021 by David Gerber - https://zapek.com + * + * This file is part of Xeres. + * + * Xeres is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Xeres is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Xeres. If not, see . + */ + +package io.xeres.app.xrs.serialization; + +import io.netty.buffer.ByteBuf; +import io.xeres.common.id.Identifier; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.lang.reflect.Field; +import java.lang.reflect.ParameterizedType; +import java.util.EnumSet; +import java.util.List; +import java.util.Map; +import java.util.Set; + +/** + * Class to serialize data types into a format compatible with + * Retroshare's wire protocol. + */ +public final class Serializer +{ + private static final Logger log = LoggerFactory.getLogger(Serializer.class); + + static final int TLV_HEADER_SIZE = 6; + + private Serializer() + { + throw new UnsupportedOperationException("Utility class"); + } + + /** + * Serializes an integer. + * + * @param buf the buffer + * @param i the integer + * @return the number of bytes taken + */ + public static int serialize(ByteBuf buf, Integer i) + { + return IntSerializer.serialize(buf, i); + } + + /** + * Deserializes an integer. + * + * @param buf the buffer + * @return the integer + */ + public static int deserializeInt(ByteBuf buf) + { + return IntSerializer.deserialize(buf); + } + + /** + * Serializes a short. + * + * @param buf the buffer + * @param sh the short + * @return the number of bytes taken + */ + public static int serialize(ByteBuf buf, Short sh) + { + return ShortSerializer.serialize(buf, sh); + } + + /** + * Deserializes a short. + * + * @param buf the buffer + * @return the short + */ + public static short deserializeShort(ByteBuf buf) + { + return ShortSerializer.deserialize(buf); + } + + /** + * Serializes a byte. + * + * @param buf the buffer + * @param b the byte + * @return the number of bytes taken + */ + public static int serialize(ByteBuf buf, Byte b) + { + return ByteSerializer.serialize(buf, b); + } + + /** + * Deserializes a byte. + * + * @param buf the buffer + * @return the byte + */ + public static byte deserializeByte(ByteBuf buf) + { + return ByteSerializer.deserialize(buf); + } + + /** + * Serializes a long. + * + * @param buf the buffer + * @param l the long + * @return the number of bytes taken + */ + public static int serialize(ByteBuf buf, Long l) + { + return LongSerializer.serialize(buf, l); + } + + /** + * Deserializes a long. + * + * @param buf the buffer + * @return the long + */ + public static long deserializeLong(ByteBuf buf) + { + return LongSerializer.deserialize(buf); + } + + /** + * Serializes a float. + * + * @param buf the buffer + * @param f the float + * @return the number of bytes taken + */ + public static int serialize(ByteBuf buf, Float f) + { + return FloatSerializer.serialize(buf, f); + } + + /** + * Deserializes a float. + * + * @param buf the buffer + * @return the float + */ + public static float deserializeFloat(ByteBuf buf) + { + return FloatSerializer.deserialize(buf); + } + + /** + * Serializes a double. + * + * @param buf the buffer + * @param d the double + * @return the number of bytes taken + */ + public static int serialize(ByteBuf buf, Double d) + { + return DoubleSerializer.serialize(buf, d); + } + + /** + * Deserializes a double. + * + * @param buf the buffer + * @return the double + */ + public static double deserializeDouble(ByteBuf buf) + { + return DoubleSerializer.deserialize(buf); + } + + /** + * Serializes a boolean. + * + * @param buf the buffer + * @param bool the boolean + * @return the number of bytes taken + */ + public static int serialize(ByteBuf buf, Boolean bool) + { + return BooleanSerializer.serialize(buf, bool); + } + + /** + * Deserializes a boolean. + * + * @param buf the buffer + * @return the boolean + */ + public static boolean deserializeBoolean(ByteBuf buf) + { + return BooleanSerializer.deserialize(buf); + } + + /** + * Serializes a string. + * + * @param buf the buffer + * @param s the string + * @return the number of bytes taken + */ + public static int serialize(ByteBuf buf, String s) + { + return StringSerializer.serialize(buf, s); + } + + /** + * Deserializes a string. + * + * @param buf the buffer + * @return the string + */ + public static String deserializeString(ByteBuf buf) + { + return StringSerializer.deserialize(buf); + } + + /** + * Serializes an identifier. + * + * @param buf the buffer + * @param identifier the identifier + * @return the number of bytes taken + */ + public static int serialize(ByteBuf buf, Identifier identifier) + { + return IdentifierSerializer.serialize(buf, identifier.getClass(), identifier); + } + + // XXX: ponder about removing the one above as this one handles null identifiers + public static int serialize(ByteBuf buf, Identifier identifier, Class identifierClass) + { + return IdentifierSerializer.serialize(buf, identifierClass, identifier); + } + + /** + * Deserializes an identifier. + * + * @param buf the buffer + * @param identifierClass the class of the identifier + * @return the identifier + */ + public static Identifier deserializeIdentifier(ByteBuf buf, Class identifierClass) + { + return IdentifierSerializer.deserialize(buf, identifierClass); + } + + /** + * Serializes a byte array. + * + * @param buf the buffer + * @param a the byte array + * @return the number of bytes taken + */ + public static int serialize(ByteBuf buf, byte[] a) + { + return ByteArraySerializer.serialize(buf, a); + } + + /** + * Deserializes a byte array. + * + * @param buf the buffer + * @return the byte array + */ + public static byte[] deserializeByteArray(ByteBuf buf) + { + return ByteArraySerializer.deserialize(buf); + } + + /** + * Serializes a map. + * + * @param buf the buffer + * @param map the map + * @return the number of bytes taken + */ + public static int serialize(ByteBuf buf, Map map) + { + return MapSerializer.serialize(buf, map); + } + + /** + * Deserializes a map. + * + * @param buf the buffer + * @param type the map key type and the map entry type + * @return the map + */ + public static Map deserializeMap(ByteBuf buf, ParameterizedType type) + { + return MapSerializer.deserialize(buf, null, type); + } + + /** + * Serializes a list. + * + * @param buf the buffer + * @param list the list + * @return the number of bytes taken + */ + public static int serialize(ByteBuf buf, List list) + { + return ListSerializer.serialize(buf, list); + } + + /** + * Deserializes a list. + * + * @param buf the buffer + * @param type the list type + * @return the list + */ + public static List deserializeList(ByteBuf buf, ParameterizedType type) + { + return ListSerializer.deserialize(buf, null, type); + } + + /** + * Serializes an enum set. + * + * @param buf the buffer + * @param enumSet the enum set + * @param fieldSize the size of the enum set bitfield + * @return the number of bytes taken + */ + public static int serialize(ByteBuf buf, Set> enumSet, FieldSize fieldSize) + { + return EnumSetSerializer.serialize(buf, enumSet, fieldSize); + } + + /** + * Deserializes an enum set. + * + * @param buf the buffer + * @param e the enum class + * @param fieldSize the size of the enum set bitfield + * @return the enum set + */ + public static > Set deserializeEnumSet(ByteBuf buf, Class e, FieldSize fieldSize) + { + return EnumSetSerializer.deserialize(buf, e, fieldSize); + } + + /** + * Serializes an enum. + * + * @param buf the buffer + * @param e the enum + * @return the number of bytes taken + */ + public static int serialize(ByteBuf buf, Enum e) + { + return EnumSerializer.serialize(buf, e); + } + + /** + * Deserializes an enum. + * + * @param buf the buffer + * @param e the enum class + * @return the enum + */ + public static > E deserializeEnum(ByteBuf buf, Class e) + { + return EnumSerializer.deserialize(buf, e); + } + + /** + * Serializes a TLV. + * + * @param buf the buffer + * @param type the type of the TLV + * @param value the value + * @return the number of bytes taken + */ + public static int serialize(ByteBuf buf, TlvType type, Object value) + { + return TlvSerializer.serialize(buf, type, value); + } + + /** + * Deserializes a TLV. + * + * @param buf the buffer + * @param type the type of the TLV + * @return the value + */ + public static Object deserialize(ByteBuf buf, TlvType type) + { + return TlvSerializer.deserialize(buf, type); + } + + /** + * Serializes a TLV binary with a defined type (needed for GXS) + * + * @param buf the buffer + * @param type the type (usually abused to be a service) + * @param data the byte array + * @return the number of bytes taken + */ + public static int serializeTlvBinary(ByteBuf buf, int type, byte[] data) + { + return TlvBinarySerializer.serializer(buf, type, data); + } + + /** + * Deserializes a TLV binary with a defined type (needed for GXS) + * + * @param buf the buffer + * @param type the type (usually abused to be a service) + * @return the byte array + */ + public static byte[] deserializeTlvBinary(ByteBuf buf, int type) + { + return TlvBinarySerializer.deserialize(buf, type); + } + + /** + * Serializes all the annotated fields of an object. + * + * @param buf the buffer + * @param object the object with the annotated fields + * @return the number of bytes taken + */ + public static int serializeAnnotatedFields(ByteBuf buf, Object object) + { + return AnnotationSerializer.serialize(buf, object); + } + + /** + * Deserializes all the annotated fields of an object. + * + * @param buf the buffer + * @param object the object with the annotated fields + * @return true if at least one field was deserialized + */ + public static boolean deserializeAnnotatedFields(ByteBuf buf, Object object) + { + return AnnotationSerializer.deserialize(buf, object); + } + + static int serialize(ByteBuf buf, Field field, Object object, RsSerialized annotation) + { + return serialize(buf, field.getType(), getField(field, object), annotation); + } + + @SuppressWarnings("unchecked") + static int serialize(ByteBuf buf, Class javaClass, Object object, RsSerialized annotation) + { + var size = 0; + + log.trace("Serializing..."); + // XXX: don't forget to handle null values! ie. object can be null (and this can't be done for primitives...) + + if (annotation != null && annotation.tlvType() != TlvType.NONE) + { + size += TlvSerializer.serialize(buf, annotation.tlvType(), object); + } + else if (Map.class.isAssignableFrom(javaClass)) + { + size += MapSerializer.serialize(buf, (Map) object); + } + else if (List.class.isAssignableFrom(javaClass)) + { + size += ListSerializer.serialize(buf, (List) object); + } + else if (EnumSet.class.isAssignableFrom(javaClass) || Set.class.isAssignableFrom(javaClass)) + { + size += EnumSetSerializer.serialize(buf, (EnumSet) object, annotation); + } + else if (Enum.class.isAssignableFrom(javaClass)) + { + size += EnumSerializer.serialize(buf, (Enum) object); + } + else if (javaClass.equals(int.class) || javaClass.equals(Integer.class)) + { + size += IntSerializer.serialize(buf, (Integer) object); + } + else if (javaClass.equals(short.class) || javaClass.equals(Short.class)) + { + size += ShortSerializer.serialize(buf, (Short) object); + } + else if (javaClass.equals(byte.class) || javaClass.equals(Byte.class)) + { + size += ByteSerializer.serialize(buf, (Byte) object); + } + else if (javaClass.equals(long.class) || javaClass.equals(Long.class)) + { + size += LongSerializer.serialize(buf, (Long) object); + } + else if (javaClass.equals(float.class) || javaClass.equals(Float.class)) + { + size += FloatSerializer.serialize(buf, (Float) object); + } + else if (javaClass.equals(double.class) || javaClass.equals(Double.class)) + { + size += DoubleSerializer.serialize(buf, (Double) object); + } + else if (javaClass.equals(boolean.class) || javaClass.equals(Boolean.class)) + { + size += BooleanSerializer.serialize(buf, (Boolean) object); + } + else if (javaClass.equals(String.class)) + { + size += StringSerializer.serialize(buf, (String) object); + } + else if (javaClass.isArray()) + { + size += ArraySerializer.serialize(buf, javaClass, object); + } + else if (Identifier.class.isAssignableFrom(javaClass)) + { + size += IdentifierSerializer.serialize(buf, javaClass, (Identifier) object); + } + else if (RsSerializable.class.isAssignableFrom(javaClass)) + { + size += RsSerializableSerializer.serialize(buf, (RsSerializable) object); + } + else + { + checkForNonAllowedType(javaClass); + size += AnnotationSerializer.serialize(buf, object); + } + return size; + } + + static void deserialize(ByteBuf buf, Field field, Object object, RsSerialized annotation) + { + setField(field, object, deserialize(buf, field.getType(), field, object, annotation)); + } + + static Object deserialize(ByteBuf buf, Class javaClass) + { + return deserialize(buf, javaClass, null, null, null); + } + + @SuppressWarnings("unchecked") + private static Object deserialize(ByteBuf buf, Class javaClass, Field field, Object object, RsSerialized annotation) + { + if (annotation != null && annotation.tlvType() != TlvType.NONE) + { + return TlvSerializer.deserialize(buf, annotation.tlvType()); + } + else if (javaClass.equals(int.class) || javaClass.equals(Integer.class)) + { + return IntSerializer.deserialize(buf); + } + else if (javaClass.equals(short.class) || javaClass.equals(Short.class)) + { + return ShortSerializer.deserialize(buf); + } + else if (javaClass.equals(byte.class) || javaClass.equals(Byte.class)) + { + return ByteSerializer.deserialize(buf); + } + else if (javaClass.equals(long.class) || javaClass.equals(Long.class)) + { + return LongSerializer.deserialize(buf); + } + else if (javaClass.equals(float.class) || javaClass.equals(Float.class)) + { + return FloatSerializer.deserialize(buf); + } + else if (javaClass.equals(double.class) || javaClass.equals(Double.class)) + { + return DoubleSerializer.deserialize(buf); + } + else if (javaClass.equals(boolean.class) || javaClass.equals(Boolean.class)) + { + return BooleanSerializer.deserialize(buf); + } + else if (javaClass.equals(String.class)) + { + return StringSerializer.deserialize(buf); + } + else if (Identifier.class.isAssignableFrom(javaClass)) + { + return IdentifierSerializer.deserialize(buf, javaClass); + } + else if (RsSerializable.class.isAssignableFrom(javaClass)) + { + return RsSerializableSerializer.deserialize(buf, javaClass); + } + else if (javaClass.isArray()) + { + return ArraySerializer.deserialize(buf, javaClass); + } + else if (Map.class.isAssignableFrom(javaClass)) + { + return MapSerializer.deserialize(buf, (Map) getField(field, object), (ParameterizedType) field.getGenericType()); + } + else if (List.class.isAssignableFrom(javaClass)) + { + return ListSerializer.deserialize(buf, (List) getField(field, object), (ParameterizedType) field.getGenericType()); + } + else if (EnumSet.class.isAssignableFrom(javaClass) || Set.class.isAssignableFrom(javaClass)) + { + return EnumSetSerializer.deserialize(buf, (ParameterizedType) field.getGenericType(), annotation); + } + else if (Enum.class.isAssignableFrom(javaClass)) + { + return EnumSerializer.deserialize(buf, javaClass); + } + else + { + checkForNonAllowedType(javaClass); + return AnnotationSerializer.deserialize(buf, javaClass); + } + } + + private static Object getField(Field field, Object object) + { + try + { + return field.get(object); + } + catch (IllegalAccessException e) + { + throw new IllegalStateException("Can't access field " + field + ": " + e.getMessage(), e); + } + } + + @SuppressWarnings("java:S3011") // Accessibility bypass + private static void setField(Field field, Object object, Object value) + { + try + { + field.set(object, value); + } + catch (IllegalAccessException e) + { + throw new IllegalStateException("Can't set field " + field + ": " + e.getMessage(), e); + } + } + + /** + * Checks that a class is allowed for serialization. Retroshare is C++ so compound types should be disallowed + * but they are used for lists and maps and we cannot check them here. + * + * @param javaClass the class to check for support, an IllegalArgumentException is thrown if it is not supported + */ + private static void checkForNonAllowedType(Class javaClass) + { + if (javaClass.equals(Character.class) + || javaClass.equals(char.class)) + { + throw new IllegalArgumentException("Class " + javaClass.getSimpleName() + " is not allowed for serialization"); + } + } +} diff --git a/app/src/main/java/io/xeres/app/xrs/serialization/ShortSerializer.java b/app/src/main/java/io/xeres/app/xrs/serialization/ShortSerializer.java new file mode 100644 index 000000000..830caf186 --- /dev/null +++ b/app/src/main/java/io/xeres/app/xrs/serialization/ShortSerializer.java @@ -0,0 +1,52 @@ +/* + * Copyright (c) 2019-2021 by David Gerber - https://zapek.com + * + * This file is part of Xeres. + * + * Xeres is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Xeres is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Xeres. If not, see . + */ + +package io.xeres.app.xrs.serialization; + +import io.netty.buffer.ByteBuf; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.Objects; + +final class ShortSerializer +{ + private static final Logger log = LoggerFactory.getLogger(ShortSerializer.class); + + private ShortSerializer() + { + throw new UnsupportedOperationException("Utility class"); + } + + static int serialize(ByteBuf buf, Short sh) + { + Objects.requireNonNull(sh, "Null short not supported"); + log.trace("Writing short: {}", sh); + buf.ensureWritable(2); + buf.writeShort(sh); + return 2; + } + + static short deserialize(ByteBuf buf) + { + var val = buf.readShort(); + log.trace("Reading short: {}", val); + return val; + } +} diff --git a/app/src/main/java/io/xeres/app/xrs/serialization/StringSerializer.java b/app/src/main/java/io/xeres/app/xrs/serialization/StringSerializer.java new file mode 100644 index 000000000..19f9de414 --- /dev/null +++ b/app/src/main/java/io/xeres/app/xrs/serialization/StringSerializer.java @@ -0,0 +1,59 @@ +/* + * Copyright (c) 2019-2021 by David Gerber - https://zapek.com + * + * This file is part of Xeres. + * + * Xeres is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Xeres is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Xeres. If not, see . + */ + +package io.xeres.app.xrs.serialization; + +import io.netty.buffer.ByteBuf; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +final class StringSerializer +{ + private static final Logger log = LoggerFactory.getLogger(StringSerializer.class); + + private StringSerializer() + { + throw new UnsupportedOperationException("Utility class"); + } + + static int serialize(ByteBuf buf, String s) + { + log.trace("Writing string: \"{}\"", s); + if (s == null) + { + buf.ensureWritable(4); + buf.writeInt(0); + return 4; + } + byte[] bytes = s.getBytes(); + buf.ensureWritable(4 + bytes.length); + buf.writeInt(bytes.length); + buf.writeBytes(bytes); + return 4 + bytes.length; + } + + static String deserialize(ByteBuf buf) + { + var len = buf.readInt(); + log.trace("Reading string of length: {}", len); + var out = new byte[len]; + buf.readBytes(out); + return new String(out); + } +} diff --git a/app/src/main/java/io/xeres/app/xrs/serialization/TlvAddressSerializer.java b/app/src/main/java/io/xeres/app/xrs/serialization/TlvAddressSerializer.java new file mode 100644 index 000000000..7ef66e1ec --- /dev/null +++ b/app/src/main/java/io/xeres/app/xrs/serialization/TlvAddressSerializer.java @@ -0,0 +1,161 @@ +/* + * Copyright (c) 2019-2021 by David Gerber - https://zapek.com + * + * This file is part of Xeres. + * + * Xeres is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Xeres is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Xeres. If not, see . + */ + +package io.xeres.app.xrs.serialization; + +import io.netty.buffer.ByteBuf; +import io.xeres.app.net.protocol.PeerAddress; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.ArrayList; +import java.util.List; + +import static io.xeres.app.xrs.serialization.TlvType.*; + +final class TlvAddressSerializer +{ + private static final Logger log = LoggerFactory.getLogger(TlvAddressSerializer.class); + + private TlvAddressSerializer() + { + throw new UnsupportedOperationException("Utility class"); + } + + static int serialize(ByteBuf buf, PeerAddress peerAddress) + { + // XXX: missing ensureWritable() + buf.writeShort(ADDRESS.getValue()); + + if (peerAddress == null) + { + buf.writeInt(6); + return 6; + } + + switch (peerAddress.getType()) + { + case IPV4 -> { + buf.writeInt(18); + buf.writeShort(IPV4.getValue()); + buf.writeInt(12); + byte[] addr = peerAddress.getAddressAsBytes().orElseThrow(); + // RS expects little endian + buf.writeByte(addr[3]); + buf.writeByte(addr[2]); + buf.writeByte(addr[1]); + buf.writeByte(addr[0]); + buf.writeByte(addr[5]); + buf.writeByte(addr[4]); + return 18; + } + default -> throw new IllegalArgumentException("Unsupported address type " + peerAddress.getType().name()); + } + } + + static PeerAddress deserialize(ByteBuf buf) + { + int type = buf.readUnsignedShort(); + log.trace("Address type: {}", type); + + if (type == ADDRESS.getValue()) + { + var totalSize = buf.readInt(); // XXX: check size + + if (totalSize > 6) + { + int addrType = buf.readUnsignedShort(); + if (addrType == IPV4.getValue()) + { + var addrSize = buf.readInt(); // XXX: check size + log.trace("reading IPv4 address of {} bytes", addrSize); + var a = new byte[6]; + + // RS stores both in little endian + a[3] = buf.readByte(); + a[2] = buf.readByte(); + a[1] = buf.readByte(); + a[0] = buf.readByte(); + + a[5] = buf.readByte(); + a[4] = buf.readByte(); + + return PeerAddress.fromByteArray(a); + } + else + { + log.debug("Skipping unsupported address type {}", addrType); + var addrSize = buf.readInt(); + buf.skipBytes(addrSize - 6); + return PeerAddress.fromInvalid(); + } + } + } + else + { + throw new IllegalArgumentException("Unrecognized address " + type); + } + return PeerAddress.fromInvalid(); + } + + static int serializeList(ByteBuf buf, List addresses) + { + // XXX: missing ensureWritable() + buf.writeShort(ADDRESS_SET.getValue()); + var totalSize = 6; + int totalSizeOffset = buf.writerIndex(); + buf.writeInt(0); + + if (addresses != null) + { + for (PeerAddress address : addresses) + { + var size = 18; + buf.writeShort(ADDRESS_INFO.getValue()); + int sizeOffset = buf.writerIndex(); + buf.writeInt(0); + size += serialize(buf, address); + buf.writeLong(0); // XXX: seenTime (64-bits)... we don't have that in PeerAddress... where do we get it from?! + buf.writeInt(0); // XXX: source (64-bits)... likewise + buf.setInt(sizeOffset, size); + totalSize += size; + } + } + buf.setInt(totalSizeOffset, totalSize); + return totalSize; + } + + static List deserializeList(ByteBuf buf) + { + int type = buf.readUnsignedShort(); + var addresses = new ArrayList(); + + if (type != ADDRESS_SET.getValue()) + { + throw new IllegalArgumentException("Type " + type + " does not match " + ADDRESS_SET.getValue()); + } + + var totalSize = buf.readInt(); // XXX: check size + log.trace("Skiping address list (for now)"); + // XXX: add code to parse multiple addresses, it's not very hard, just call the unserialize above + // XXX: don't forget there can be empty addresses... so probably remove the invalid ones + + return addresses; + } +} diff --git a/app/src/main/java/io/xeres/app/xrs/serialization/TlvBinarySerializer.java b/app/src/main/java/io/xeres/app/xrs/serialization/TlvBinarySerializer.java new file mode 100644 index 000000000..43434669d --- /dev/null +++ b/app/src/main/java/io/xeres/app/xrs/serialization/TlvBinarySerializer.java @@ -0,0 +1,95 @@ +/* + * Copyright (c) 2019-2021 by David Gerber - https://zapek.com + * + * This file is part of Xeres. + * + * Xeres is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Xeres is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Xeres. If not, see . + */ + +package io.xeres.app.xrs.serialization; + +import io.netty.buffer.ByteBuf; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import static io.xeres.app.xrs.serialization.Serializer.TLV_HEADER_SIZE; + +final class TlvBinarySerializer +{ + private static final Logger log = LoggerFactory.getLogger(TlvBinarySerializer.class); + + private TlvBinarySerializer() + { + throw new UnsupportedOperationException("Utility class"); + } + + static int serializer(ByteBuf buf, byte[] data) + { + return serializer(buf, TlvType.NONE, data); + } + + static int serializer(ByteBuf buf, TlvType type, byte[] data) + { + return serializer(buf, type.getValue(), data); + } + + static int serializer(ByteBuf buf, int type, byte[] data) + { + if (data == null) + { + data = new byte[0]; + } + + int len = getSize(data); + log.trace("Writing TLV binary data (size: {})", data.length); + buf.ensureWritable(len); + buf.writeShort(type); + buf.writeInt(len); + if (data.length > 0) + { + buf.writeBytes(data); + } + return len; + } + + static int getSize(byte[] data) + { + return TLV_HEADER_SIZE + (data != null ? data.length : 0); + } + + static byte[] deserialize(ByteBuf buf) + { + return deserialize(buf, TlvType.NONE); + } + + static byte[] deserialize(ByteBuf buf, TlvType type) + { + log.trace("Reading TLV binary"); + var len = TlvUtils.checkTypeAndLength(buf, type); + log.trace(" of {} bytes", len); + var out = new byte[len]; + buf.readBytes(out); + return out; + } + + static byte[] deserialize(ByteBuf buf, int type) + { + log.trace("Reading TLV binary"); + var len = TlvUtils.checkTypeAndLength(buf, type); + log.trace(" of {} bytes", len); + var out = new byte[len]; + buf.readBytes(out); + return out; + } +} diff --git a/app/src/main/java/io/xeres/app/xrs/serialization/TlvSecurityKeySerializer.java b/app/src/main/java/io/xeres/app/xrs/serialization/TlvSecurityKeySerializer.java new file mode 100644 index 000000000..1832ccdfb --- /dev/null +++ b/app/src/main/java/io/xeres/app/xrs/serialization/TlvSecurityKeySerializer.java @@ -0,0 +1,84 @@ +/* + * Copyright (c) 2019-2021 by David Gerber - https://zapek.com + * + * This file is part of Xeres. + * + * Xeres is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Xeres is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Xeres. If not, see . + */ + +package io.xeres.app.xrs.serialization; + +import io.netty.buffer.ByteBuf; +import io.xeres.app.xrs.common.SecurityKey; +import io.xeres.common.id.GxsId; +import io.xeres.common.id.Id; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.EnumSet; + +import static io.xeres.app.xrs.serialization.Serializer.*; +import static io.xeres.app.xrs.serialization.TlvType.*; + +final class TlvSecurityKeySerializer +{ + private static final Logger log = LoggerFactory.getLogger(TlvSecurityKeySerializer.class); + + private TlvSecurityKeySerializer() + { + throw new UnsupportedOperationException("Utility class"); + } + + static int serialize(ByteBuf buf, SecurityKey securityKey) + { + log.trace("Writing TlvRsaKey"); + + int len = getSize(securityKey); + buf.ensureWritable(len); + buf.writeShort(SECURITY_KEY.getValue()); + buf.writeInt(len); + TlvSerializer.serialize(buf, STR_KEY_ID, Id.toString(securityKey.getGxsId())); // XXX: RS uses some weird bytes to ascii here... not sure this is correct :-/ (I first used Id.toAsciiBytes() but it wants a string. see deserialization) + + Serializer.serialize(buf, EnumSet.of(SecurityKey.Flags.DISTRIBUTION_ADMIN), FieldSize.INTEGER); // keyFlags (XXX: set them to TSTLV_KEY_DISTRIB_ADMIN, possibly others) + Serializer.serialize(buf, 0); // startTS (XXX: I think it stays at 0) + Serializer.serialize(buf, 0); // endTS (XXX: I think it stays at 0) + + TlvSerializer.serialize(buf, KEY_EVP_PKEY, securityKey.getData()); + return len; + } + + static int getSize(SecurityKey securityKey) + { + return TLV_HEADER_SIZE + + TlvSerializer.getSize(STR_KEY_ID) + + 4 + + 4 + + 4 + + TLV_HEADER_SIZE + securityKey.getData().length; // XXX: add a getSize() accessor + } + + static SecurityKey deserialize(ByteBuf buf) + { + log.trace("Reading TlvRsaKey"); + + TlvUtils.checkTypeAndLength(buf, SECURITY_KEY); + var gxsId = new GxsId(Id.asciiStringToBytes((String) TlvSerializer.deserialize(buf, STR_KEY_ID))); + var flags = deserializeEnumSet(buf, SecurityKey.Flags.class, FieldSize.INTEGER); + var startTs = deserializeInt(buf); + var endTs = deserializeInt(buf); + + var data = (byte[]) TlvSerializer.deserialize(buf, KEY_EVP_PKEY); + return new SecurityKey(gxsId, flags, startTs, endTs, data); + } +} diff --git a/app/src/main/java/io/xeres/app/xrs/serialization/TlvSecurityKeySetSerializer.java b/app/src/main/java/io/xeres/app/xrs/serialization/TlvSecurityKeySetSerializer.java new file mode 100644 index 000000000..8b40df03d --- /dev/null +++ b/app/src/main/java/io/xeres/app/xrs/serialization/TlvSecurityKeySetSerializer.java @@ -0,0 +1,82 @@ +/* + * Copyright (c) 2019-2021 by David Gerber - https://zapek.com + * + * This file is part of Xeres. + * + * Xeres is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Xeres is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Xeres. If not, see . + */ + +package io.xeres.app.xrs.serialization; + +import io.netty.buffer.ByteBuf; +import io.xeres.app.xrs.common.SecurityKey; +import io.xeres.app.xrs.common.SecurityKeySet; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import static io.xeres.app.xrs.serialization.Serializer.TLV_HEADER_SIZE; +import static io.xeres.app.xrs.serialization.TlvType.*; + +final class TlvSecurityKeySetSerializer +{ + private static final Logger log = LoggerFactory.getLogger(TlvSecurityKeySetSerializer.class); + + private TlvSecurityKeySetSerializer() + { + throw new UnsupportedOperationException("Utility class"); + } + + static int serialize(ByteBuf buf, SecurityKeySet securityKeySet) + { + log.trace("Writing TlvSecurityKeySet"); + + int len = getSize(securityKeySet); + buf.ensureWritable(len); + buf.writeShort(SECURITY_KEY_SET.getValue()); + buf.writeInt(len); + TlvSerializer.serialize(buf, STR_GROUP_ID, securityKeySet.getGroupId()); + securityKeySet.getPublicKeys().forEach((gxsId, securityKey) -> TlvSerializer.serialize(buf, SECURITY_KEY, securityKey)); + securityKeySet.getPrivateKeys().forEach((gxsId, securityKey) -> TlvSerializer.serialize(buf, SECURITY_KEY, securityKey)); + + return len; + } + + static int getSize(SecurityKeySet securityKeySet) + { + return TLV_HEADER_SIZE + + TlvStringSerializer.getSize(securityKeySet.getGroupId()) + + securityKeySet.getPublicKeys().values().stream().mapToInt(publicKey -> TlvSerializer.getSize(SECURITY_KEY, publicKey)).sum() + + securityKeySet.getPrivateKeys().values().stream().mapToInt(privateKey -> TlvSerializer.getSize(SECURITY_KEY, privateKey)).sum(); + } + + static SecurityKeySet deserialize(ByteBuf buf) + { + log.trace("Reading TlvSecurityKeySet"); + + var len = TlvUtils.checkTypeAndLength(buf, SECURITY_KEY_SET); + + TlvSerializer.deserialize(buf, STR_GROUP_ID); + len -= TLV_HEADER_SIZE; // XXX: this is correct, but we'd better just read the STR_GROUP_ID and check it's size just in case (even though it's empty) + + var securityKeySet = new SecurityKeySet(); + while (len > 0) + { + // XXX: here we should check for SECURITY_KEY and skip if the TLV type is unknown in case keys are upgraded in the future + var securityKey = (SecurityKey) TlvSerializer.deserialize(buf, SECURITY_KEY); + securityKeySet.put(securityKey); + len -= TlvSerializer.getSize(SECURITY_KEY, securityKey); + } + return securityKeySet; + } +} diff --git a/app/src/main/java/io/xeres/app/xrs/serialization/TlvSerializer.java b/app/src/main/java/io/xeres/app/xrs/serialization/TlvSerializer.java new file mode 100644 index 000000000..15cf8b5cf --- /dev/null +++ b/app/src/main/java/io/xeres/app/xrs/serialization/TlvSerializer.java @@ -0,0 +1,104 @@ +/* + * Copyright (c) 2019-2021 by David Gerber - https://zapek.com + * + * This file is part of Xeres. + * + * Xeres is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Xeres is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Xeres. If not, see . + */ + +package io.xeres.app.xrs.serialization; + +import io.netty.buffer.ByteBuf; +import io.xeres.app.net.protocol.PeerAddress; +import io.xeres.app.xrs.common.SecurityKey; +import io.xeres.app.xrs.common.SecurityKeySet; +import io.xeres.app.xrs.common.Signature; +import io.xeres.app.xrs.common.SignatureSet; +import io.xeres.common.id.GxsId; + +import java.util.List; +import java.util.Set; + +import static io.xeres.app.xrs.serialization.Serializer.TLV_HEADER_SIZE; +import static io.xeres.app.xrs.serialization.TlvType.SIGNATURE_TYPE; + +final class TlvSerializer +{ + private TlvSerializer() + { + throw new UnsupportedOperationException("Utility class"); + } + + @SuppressWarnings("unchecked") + static int serialize(ByteBuf buf, TlvType type, Object value) + { + return switch (type) + { + case STR_NAME, STR_MSG, STR_LOCATION, STR_VERSION, STR_HASH_SHA1, STR_DYNDNS, STR_DOM_ADDR, STR_GENID, STR_KEY_ID, STR_GROUP_ID -> TlvStringSerializer.serialize(buf, type, (String) value); + case ADDRESS -> TlvAddressSerializer.serialize(buf, (PeerAddress) value); + case ADDRESS_SET -> TlvAddressSerializer.serializeList(buf, (List) value); + case SIGNATURE -> TlvSignatureSerializer.serialize(buf, (Signature) value); + case SET_PGP_ID -> TlvSetSerializer.serializeLong(buf, type, (Set) value); + case SET_RECOGN -> TlvStringSetRefSerializer.serialize(buf, type, (List) value); + case STRING -> TlvStringSerializer.serialize(buf, TlvType.NONE, (String) value); + case SIGNATURE_SET -> TlvSignatureSetSerializer.serialize(buf, (SignatureSet) value); + case SIGNATURE_TYPE -> TlvUint32Serializer.serialize(buf, SIGNATURE_TYPE, (Integer) value); + case SECURITY_KEY -> TlvSecurityKeySerializer.serialize(buf, (SecurityKey) value); + case SECURITY_KEY_SET -> TlvSecurityKeySetSerializer.serialize(buf, (SecurityKeySet) value); + case SIGN_RSA_SHA1, KEY_EVP_PKEY, STR_SIGN -> TlvBinarySerializer.serializer(buf, type, (byte[]) value); + case IPV4, IPV6, ADDRESS_INFO, NONE -> throw new IllegalArgumentException("Can't use type " + type + " for direct TLV serialization"); + }; + } + + static int getSize(TlvType type, Object value) + { + return switch (type) + { + case SIGN_RSA_SHA1 -> TlvBinarySerializer.getSize((byte[]) value); + case SIGNATURE -> TlvSignatureSerializer.getSize((Signature) value); + case SECURITY_KEY -> TlvSecurityKeySerializer.getSize((SecurityKey) value); + default -> throw new IllegalArgumentException("Not implemented for type " + type); + }; + } + + static int getSize(TlvType type) + { + return switch (type) + { + case STR_KEY_ID -> TLV_HEADER_SIZE + GxsId.LENGTH * 2; + case SIGNATURE_TYPE -> TlvUint32Serializer.getSize(); + default -> throw new IllegalArgumentException("Not implemented for type " + type); + }; + } + + static Object deserialize(ByteBuf buf, TlvType type) + { + return switch (type) + { + case STR_NAME, STR_MSG, STR_LOCATION, STR_VERSION, STR_HASH_SHA1, STR_DYNDNS, STR_DOM_ADDR, STR_GENID, STR_KEY_ID, STR_GROUP_ID -> TlvStringSerializer.deserialize(buf, type); + case ADDRESS -> TlvAddressSerializer.deserialize(buf); + case ADDRESS_SET -> TlvAddressSerializer.deserializeList(buf); + case SIGNATURE -> TlvSignatureSerializer.deserialize(buf); + case SET_PGP_ID -> TlvSetSerializer.deserializeLong(buf, type); + case SET_RECOGN -> TlvStringSetRefSerializer.deserialize(buf, type); + case STRING -> TlvStringSerializer.deserialize(buf, TlvType.NONE); + case SIGNATURE_SET -> TlvSignatureSetSerializer.deserialize(buf); + case SIGNATURE_TYPE -> TlvUint32Serializer.deserialize(buf, SIGNATURE_TYPE); + case SECURITY_KEY -> TlvSecurityKeySerializer.deserialize(buf); + case SECURITY_KEY_SET -> TlvSecurityKeySetSerializer.deserialize(buf); + case SIGN_RSA_SHA1, KEY_EVP_PKEY, STR_SIGN -> TlvBinarySerializer.deserialize(buf, type); + case IPV4, IPV6, ADDRESS_INFO, NONE -> throw new IllegalArgumentException("Can't use type " + type + " for direct TLV deserialization"); + }; + } +} diff --git a/app/src/main/java/io/xeres/app/xrs/serialization/TlvSetSerializer.java b/app/src/main/java/io/xeres/app/xrs/serialization/TlvSetSerializer.java new file mode 100644 index 000000000..06c672bec --- /dev/null +++ b/app/src/main/java/io/xeres/app/xrs/serialization/TlvSetSerializer.java @@ -0,0 +1,70 @@ +/* + * Copyright (c) 2019-2021 by David Gerber - https://zapek.com + * + * This file is part of Xeres. + * + * Xeres is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Xeres is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Xeres. If not, see . + */ + +package io.xeres.app.xrs.serialization; + +import io.netty.buffer.ByteBuf; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.Arrays; +import java.util.HashSet; +import java.util.Set; + +import static io.xeres.app.xrs.serialization.Serializer.TLV_HEADER_SIZE; + +final class TlvSetSerializer +{ + private static final Logger log = LoggerFactory.getLogger(TlvSetSerializer.class); + + private TlvSetSerializer() + { + throw new UnsupportedOperationException("Utility class"); + } + + static int serializeLong(ByteBuf buf, TlvType type, Set set) + { + int len = getSize(set); + log.trace("Writing set of longs: {}", log.isTraceEnabled() ? Arrays.toString(set.toArray()) : ""); + buf.ensureWritable(len); + buf.writeShort(type.getValue()); + buf.writeInt(len); + set.forEach(buf::writeLong); + return len; + } + + static int getSize(Set set) + { + return TLV_HEADER_SIZE + 8 * set.size(); + } + + static Set deserializeLong(ByteBuf buf, TlvType type) + { + log.trace("Reading set of longs"); + var len = TlvUtils.checkTypeAndLength(buf, type); + int count = len / 8; + var set = new HashSet(count); + + while (count-- > 0) + { + set.add(buf.readLong()); + } + return set; + } +} diff --git a/app/src/main/java/io/xeres/app/xrs/serialization/TlvSignatureSerializer.java b/app/src/main/java/io/xeres/app/xrs/serialization/TlvSignatureSerializer.java new file mode 100644 index 000000000..982d6644e --- /dev/null +++ b/app/src/main/java/io/xeres/app/xrs/serialization/TlvSignatureSerializer.java @@ -0,0 +1,71 @@ +/* + * Copyright (c) 2019-2021 by David Gerber - https://zapek.com + * + * This file is part of Xeres. + * + * Xeres is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Xeres is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Xeres. If not, see . + */ + +package io.xeres.app.xrs.serialization; + +import io.netty.buffer.ByteBuf; +import io.xeres.app.xrs.common.Signature; +import io.xeres.common.id.GxsId; +import io.xeres.common.id.Id; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import static io.xeres.app.xrs.serialization.Serializer.TLV_HEADER_SIZE; +import static io.xeres.app.xrs.serialization.TlvType.*; + +final class TlvSignatureSerializer +{ + private static final Logger log = LoggerFactory.getLogger(TlvSignatureSerializer.class); + + private TlvSignatureSerializer() + { + throw new UnsupportedOperationException("Utility class"); + } + + static int serialize(ByteBuf buf, Signature signature) + { + log.trace("Writing TlvKeySignature"); + + int len = getSize(signature); + buf.ensureWritable(len); + buf.writeShort(SIGNATURE.getValue()); + buf.writeInt(len); + TlvSerializer.serialize(buf, STR_KEY_ID, Id.toString(signature.getGxsId())); + TlvSerializer.serialize(buf, SIGN_RSA_SHA1, signature.getData()); + + return len; + } + + static int getSize(Signature signature) + { + return TLV_HEADER_SIZE + + TlvSerializer.getSize(STR_KEY_ID) + + TlvSerializer.getSize(SIGN_RSA_SHA1, signature.getData()); + } + + static Signature deserialize(ByteBuf buf) + { + log.trace("Reading TlvKeySignature"); + + TlvUtils.checkTypeAndLength(buf, SIGNATURE); + var gxsId = new GxsId(Id.asciiStringToBytes((String) TlvSerializer.deserialize(buf, STR_KEY_ID))); + var data = (byte[]) TlvSerializer.deserialize(buf, SIGN_RSA_SHA1); + return new Signature(gxsId, data); + } +} diff --git a/app/src/main/java/io/xeres/app/xrs/serialization/TlvSignatureSetSerializer.java b/app/src/main/java/io/xeres/app/xrs/serialization/TlvSignatureSetSerializer.java new file mode 100644 index 000000000..f4dfa2484 --- /dev/null +++ b/app/src/main/java/io/xeres/app/xrs/serialization/TlvSignatureSetSerializer.java @@ -0,0 +1,77 @@ +/* + * Copyright (c) 2019-2021 by David Gerber - https://zapek.com + * + * This file is part of Xeres. + * + * Xeres is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Xeres is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Xeres. If not, see . + */ + +package io.xeres.app.xrs.serialization; + +import io.netty.buffer.ByteBuf; +import io.xeres.app.xrs.common.Signature; +import io.xeres.app.xrs.common.SignatureSet; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import static io.xeres.app.xrs.serialization.Serializer.TLV_HEADER_SIZE; +import static io.xeres.app.xrs.serialization.TlvType.*; + +final class TlvSignatureSetSerializer +{ + private static final Logger log = LoggerFactory.getLogger(TlvSignatureSetSerializer.class); + + private TlvSignatureSetSerializer() + { + throw new UnsupportedOperationException("Utility class"); + } + + static int serialize(ByteBuf buf, SignatureSet signatureSet) + { + log.trace("Writing TlvSignatureSet"); + + int len = getSize(signatureSet); + buf.ensureWritable(len); + buf.writeShort(SIGNATURE_SET.getValue()); + buf.writeInt(len); + signatureSet.getSignatures().forEach((signType, keySignature) -> { + TlvSerializer.serialize(buf, SIGNATURE_TYPE, signType); + TlvSerializer.serialize(buf, SIGNATURE, keySignature); + }); + + return len; + } + + static int getSize(SignatureSet signatureSet) + { + return TLV_HEADER_SIZE + + signatureSet.getSignatures().values().stream().mapToInt(signature -> TlvSerializer.getSize(SIGNATURE_TYPE) + TlvSerializer.getSize(SIGNATURE, signature)).sum(); + } + + static SignatureSet deserialize(ByteBuf buf) + { + log.trace("Reading TlvSignatureSet"); + var len = TlvUtils.checkTypeAndLength(buf, SIGNATURE_SET); + + var keySignatureSet = new SignatureSet(); + while (len > 0) + { + SignatureSet.Type type = SignatureSet.Type.findByValue((int) TlvSerializer.deserialize(buf, SIGNATURE_TYPE)); + var keySignature = (Signature) TlvSerializer.deserialize(buf, SIGNATURE); + keySignatureSet.put(type, keySignature); + len -= TlvSerializer.getSize(SIGNATURE_TYPE) + TlvSerializer.getSize(SIGNATURE, keySignature); + } + return keySignatureSet; + } +} diff --git a/app/src/main/java/io/xeres/app/xrs/serialization/TlvStringSerializer.java b/app/src/main/java/io/xeres/app/xrs/serialization/TlvStringSerializer.java new file mode 100644 index 000000000..b017424ff --- /dev/null +++ b/app/src/main/java/io/xeres/app/xrs/serialization/TlvStringSerializer.java @@ -0,0 +1,67 @@ +/* + * Copyright (c) 2019-2021 by David Gerber - https://zapek.com + * + * This file is part of Xeres. + * + * Xeres is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Xeres is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Xeres. If not, see . + */ + +package io.xeres.app.xrs.serialization; + +import io.netty.buffer.ByteBuf; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import static io.xeres.app.xrs.serialization.Serializer.TLV_HEADER_SIZE; + +final class TlvStringSerializer +{ + private static final Logger log = LoggerFactory.getLogger(TlvStringSerializer.class); + + private TlvStringSerializer() + { + throw new UnsupportedOperationException("Utility class"); + } + + static int serialize(ByteBuf buf, TlvType type, String s) + { + int len = getSize(s); + + byte[] bytes = s != null ? s.getBytes() : new byte[0]; + + log.trace("Writing string ({}): \"{}\"", type, s); + buf.ensureWritable(len); + buf.writeShort(type.getValue()); + buf.writeInt(len); + if (bytes.length > 0) + { + buf.writeBytes(bytes); + } + return len; + } + + static int getSize(String s) + { + return TLV_HEADER_SIZE + (s != null ? s.getBytes().length : 0); + } + + static String deserialize(ByteBuf buf, TlvType type) + { + log.trace("Reading TLV string"); + var len = TlvUtils.checkTypeAndLength(buf, type); + var out = new byte[len]; + buf.readBytes(out); + return new String(out); + } +} diff --git a/app/src/main/java/io/xeres/app/xrs/serialization/TlvStringSetRefSerializer.java b/app/src/main/java/io/xeres/app/xrs/serialization/TlvStringSetRefSerializer.java new file mode 100644 index 000000000..e23d3834e --- /dev/null +++ b/app/src/main/java/io/xeres/app/xrs/serialization/TlvStringSetRefSerializer.java @@ -0,0 +1,69 @@ +/* + * Copyright (c) 2019-2021 by David Gerber - https://zapek.com + * + * This file is part of Xeres. + * + * Xeres is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Xeres is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Xeres. If not, see . + */ + +package io.xeres.app.xrs.serialization; + +import io.netty.buffer.ByteBuf; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.ArrayList; +import java.util.List; + +import static io.xeres.app.xrs.serialization.Serializer.TLV_HEADER_SIZE; + +final class TlvStringSetRefSerializer +{ + private static final Logger log = LoggerFactory.getLogger(TlvStringSetRefSerializer.class); + + private TlvStringSetRefSerializer() + { + throw new UnsupportedOperationException("Utility class"); + } + + // XXX: warning! serialization has not been tested + static int serialize(ByteBuf buf, TlvType type, List refIds) + { + int len = getSize(refIds); + log.trace("Writing refids: {}", log.isTraceEnabled() ? refIds : ""); + buf.ensureWritable(len); + buf.writeShort(type.getValue()); + buf.writeInt(len); + refIds.forEach(s -> TlvSerializer.serialize(buf, TlvType.STR_GENID, s)); + return len; + } + + static int getSize(List refIds) + { + return TLV_HEADER_SIZE + (int) refIds.stream().map(s -> TLV_HEADER_SIZE + s.length()).count(); + } + + static List deserialize(ByteBuf buf, TlvType type) + { + log.trace("Reading refids"); + var len = TlvUtils.checkTypeAndLength(buf, type); + int listIndex = buf.readerIndex(); + List refIds = new ArrayList<>(); + while (buf.readerIndex() < listIndex + len) + { + refIds.add((String) Serializer.deserialize(buf, TlvType.STR_GENID)); + } + return refIds; + } +} diff --git a/app/src/main/java/io/xeres/app/xrs/serialization/TlvType.java b/app/src/main/java/io/xeres/app/xrs/serialization/TlvType.java new file mode 100644 index 000000000..01462fae2 --- /dev/null +++ b/app/src/main/java/io/xeres/app/xrs/serialization/TlvType.java @@ -0,0 +1,63 @@ +/* + * Copyright (c) 2019-2021 by David Gerber - https://zapek.com + * + * This file is part of Xeres. + * + * Xeres is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Xeres is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Xeres. If not, see . + */ + +package io.xeres.app.xrs.serialization; + +public enum TlvType +{ + NONE(0x0), + STR_NAME(0x51), + STR_MSG(0x57), + STR_GENID(0x5a), + STR_LOCATION(0x5c), + STR_VERSION(0x5f), + STR_HASH_SHA1(0x70), + STR_DYNDNS(0x83), + STR_DOM_ADDR(0x84), + IPV4(0x85), + IPV6(0x86), + STR_GROUP_ID(0xa0), + STR_KEY_ID(0xa4), + STR_SIGN(0xb4), + KEY_EVP_PKEY(0x110), + SIGN_RSA_SHA1(0x120), + SET_PGP_ID(0x1023), + SET_RECOGN(0x1024), + SECURITY_KEY(0x1040), + SECURITY_KEY_SET(0x1041), + SIGNATURE(0x1050), + SIGNATURE_SET(0x1051), + SIGNATURE_TYPE(0x1052), + ADDRESS_INFO(0x1070), + ADDRESS_SET(0x1071), + ADDRESS(0x1072), + STRING(0x2223); // not an official RS tlv, used to write string with type 0 (XXX: make sure RS really does that!) + + private final int value; + + TlvType(int value) + { + this.value = value; + } + + public int getValue() + { + return value; + } +} diff --git a/app/src/main/java/io/xeres/app/xrs/serialization/TlvUint32Serializer.java b/app/src/main/java/io/xeres/app/xrs/serialization/TlvUint32Serializer.java new file mode 100644 index 000000000..ff60c1779 --- /dev/null +++ b/app/src/main/java/io/xeres/app/xrs/serialization/TlvUint32Serializer.java @@ -0,0 +1,62 @@ +/* + * Copyright (c) 2019-2021 by David Gerber - https://zapek.com + * + * This file is part of Xeres. + * + * Xeres is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Xeres is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Xeres. If not, see . + */ + +package io.xeres.app.xrs.serialization; + +import io.netty.buffer.ByteBuf; + +import static io.xeres.app.xrs.serialization.Serializer.TLV_HEADER_SIZE; + +final class TlvUint32Serializer +{ + private TlvUint32Serializer() + { + throw new UnsupportedOperationException("Utility class"); + } + + static int serialize(ByteBuf buf, TlvType type, int value) + { + int len = getSize(); + buf.ensureWritable(len); + buf.writeShort(type.getValue()); + buf.writeInt(len); + buf.writeInt(value); + return len; + } + + static int getSize() + { + return TLV_HEADER_SIZE + 4; + } + + static int deserialize(ByteBuf buf, TlvType type) + { + int readType = buf.readUnsignedShort(); + if (readType != type.getValue()) + { + throw new IllegalArgumentException("Type " + readType + " does not match " + type); + } + var len = buf.readInt(); + if (len != getSize()) + { + throw new IllegalArgumentException("Length is wrong: " + len + ", expected: " + getSize()); + } + return buf.readInt(); + } +} diff --git a/app/src/main/java/io/xeres/app/xrs/serialization/TlvUtils.java b/app/src/main/java/io/xeres/app/xrs/serialization/TlvUtils.java new file mode 100644 index 000000000..e3550a1d0 --- /dev/null +++ b/app/src/main/java/io/xeres/app/xrs/serialization/TlvUtils.java @@ -0,0 +1,77 @@ +/* + * Copyright (c) 2019-2021 by David Gerber - https://zapek.com + * + * This file is part of Xeres. + * + * Xeres is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Xeres is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Xeres. If not, see . + */ + +package io.xeres.app.xrs.serialization; + +import io.netty.buffer.ByteBuf; + +import static io.xeres.app.xrs.serialization.Serializer.TLV_HEADER_SIZE; + +final class TlvUtils +{ + private TlvUtils() + { + throw new UnsupportedOperationException("Utility class"); + } + + /** + * Checks if the buffer contains the right TlvType and if the length is at least TLV_HEADER_SIZE. + * + * @param buf the ByteBuf containing the incoming data + * @param tlvType the TlvType to check against + * @return the remaining length after TLV_HEADER_SIZE is subtracted + */ + static int checkTypeAndLength(ByteBuf buf, TlvType tlvType) + { + int readType = buf.readUnsignedShort(); + if (readType != tlvType.getValue()) + { + throw new IllegalArgumentException("Type " + readType + " does not match " + tlvType); + } + var len = buf.readInt(); + if (len < TLV_HEADER_SIZE) + { + throw new IllegalArgumentException("Length " + len + " is smaller than the header size (6)"); + } + return len - TLV_HEADER_SIZE; + } + + /** + * Checks if the buffer contains the right TLV type and if the length is at least TLV_HEADER_SIZE. + * This function is needed in addition to the one above because Retroshare abuses some TLVs to store the service type in them. + * + * @param buf the ByteBuf containing the incoming data + * @param tlvType the TLV type to check against, as an int + * @return the remaining length after TLV_HEADER_SIZE is subtracted + */ + static int checkTypeAndLength(ByteBuf buf, int tlvType) + { + int readType = buf.readUnsignedShort(); + if (readType != tlvType) + { + throw new IllegalArgumentException("Type " + readType + " does not match " + tlvType); + } + var len = buf.readInt(); + if (len < TLV_HEADER_SIZE) + { + throw new IllegalArgumentException("Length " + len + " is smaller than the header size (6)"); + } + return len - TLV_HEADER_SIZE; + } +} diff --git a/app/src/main/java/io/xeres/app/xrs/service/RsService.java b/app/src/main/java/io/xeres/app/xrs/service/RsService.java new file mode 100644 index 000000000..d464fa37f --- /dev/null +++ b/app/src/main/java/io/xeres/app/xrs/service/RsService.java @@ -0,0 +1,160 @@ +/* + * Copyright (c) 2019-2021 by David Gerber - https://zapek.com + * + * This file is part of Xeres. + * + * Xeres is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Xeres is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Xeres. If not, see . + */ + +package io.xeres.app.xrs.service; + +import io.netty.channel.ChannelFuture; +import io.xeres.app.database.model.location.Location; +import io.xeres.app.net.peer.PeerConnection; +import io.xeres.app.net.peer.PeerConnectionManager; +import io.xeres.app.xrs.item.Item; +import org.springframework.core.env.Environment; + +import javax.annotation.PostConstruct; +import javax.annotation.PreDestroy; +import java.lang.reflect.InvocationTargetException; +import java.util.HashMap; +import java.util.Locale; +import java.util.Map; + +public abstract class RsService implements Comparable +{ + private final Map> searchBySubType = new HashMap<>(); + private Map, Integer> searchByClass = new HashMap<>(); + + public abstract RsServiceType getServiceType(); + + public abstract Map, Integer> getSupportedItems(); + + public abstract void handleItem(PeerConnection peerConnection, Item item); + + private final Environment environment; + private final PeerConnectionManager peerConnectionManager; + + protected RsService(Environment environment, PeerConnectionManager peerConnectionManager) + { + this.environment = environment; + this.peerConnectionManager = peerConnectionManager; + } + + public RsServiceInitPriority getInitPriority() + { + return RsServiceInitPriority.OFF; + } + + public int getItemSubtype(Item item) + { + return searchByClass.get(item.getClass()); + } + + /** + * Sent once upon startup when the service is enabled. Good place to initialize + * executors, etc... + */ + public void initialize() + { + // Do nothing by default + } + + /** + * Sent once when the application is exiting but before closing the connections. + * Good place to send last messages (eg. leaving a room, etc...). + */ + public void shutdown() + { + // Do nothing by default + } + + /** + * Sent once when the application is almost done exiting. Good place to remove any + * executor setup in initialize(). + */ + public void cleanup() + { + // Do nothing by default + } + + public void initialize(PeerConnection peerConnection) + { + throw new IllegalStateException("Implement initialize() method if you override getInitPriority() to be anything else than OFF"); + } + + @PostConstruct + private void init() + { + if (Boolean.TRUE.equals(environment.getProperty(getPropertyName(), Boolean.class, false))) + { + RsServiceRegistry.registerService(getServiceType().getType(), this); + searchByClass = getSupportedItems(); + getSupportedItems().forEach((itemClass, itemSubType) -> + { + try + { + itemClass.getConstructor(); + } + catch (NoSuchMethodException e) + { + throw new IllegalArgumentException(itemClass.getSimpleName() + " requires a public constructor with no parameters"); + } + searchBySubType.put(itemSubType, itemClass); + }); + + initialize(); + } + } + + @PreDestroy + private void destroy() + { + cleanup(); + } + + public Item createItem(int subType) throws NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException + { + Class itemClass = searchBySubType.get(subType); + if (itemClass == null) + { + throw new InstantiationException("No such type " + subType); + } + return itemClass.getConstructor().newInstance(); + } + + private String getPropertyName() + { + String className = getClass().getSimpleName(); + assert className.endsWith("Service"); + return "xrs.service." + className.substring(0, className.length() - 7).toLowerCase(Locale.ROOT) + ".enabled"; + } + + protected ChannelFuture writeItem(PeerConnection peerConnection, Item item) + { + return peerConnectionManager.writeItem(peerConnection, item, this); + } + + protected ChannelFuture writeItem(Location location, Item item) + { + return peerConnectionManager.writeItem(location, item, this); + } + + @Override + public int compareTo(RsService o) + { + return Integer.compare(getInitPriority().ordinal(), o.getInitPriority().ordinal()); + } +} diff --git a/app/src/main/java/io/xeres/app/xrs/service/RsServiceInitPriority.java b/app/src/main/java/io/xeres/app/xrs/service/RsServiceInitPriority.java new file mode 100644 index 000000000..40b512d0a --- /dev/null +++ b/app/src/main/java/io/xeres/app/xrs/service/RsServiceInitPriority.java @@ -0,0 +1,48 @@ +/* + * Copyright (c) 2019-2020 by David Gerber - https://zapek.com + * + * This file is part of Xeres. + * + * Xeres is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Xeres is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Xeres. If not, see . + */ + +package io.xeres.app.xrs.service; + +public enum RsServiceInitPriority +{ + OFF(0, 0), + LOW(11, 20), + NORMAL(6, 10), + HIGH(3, 5), + IMMEDIATE(1, 2); + + private final int minTime; + private final int maxTime; + + RsServiceInitPriority(int minTime, int maxTime) + { + this.minTime = minTime; + this.maxTime = maxTime; + } + + public int getMinTime() + { + return minTime; + } + + public int getMaxTime() + { + return maxTime; + } +} diff --git a/app/src/main/java/io/xeres/app/xrs/service/RsServiceRegistry.java b/app/src/main/java/io/xeres/app/xrs/service/RsServiceRegistry.java new file mode 100644 index 000000000..fbe745c95 --- /dev/null +++ b/app/src/main/java/io/xeres/app/xrs/service/RsServiceRegistry.java @@ -0,0 +1,50 @@ +/* + * Copyright (c) 2019-2021 by David Gerber - https://zapek.com + * + * This file is part of Xeres. + * + * Xeres is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Xeres is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Xeres. If not, see . + */ + +package io.xeres.app.xrs.service; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +public final class RsServiceRegistry +{ + private RsServiceRegistry() + { + throw new UnsupportedOperationException("Utility class"); + } + + private static final Map services = new HashMap<>(); + + public static List getServices() + { + return new ArrayList<>(services.values()); + } + + public static RsService getServiceFromType(int type) + { + return services.get(type); + } + + public static void registerService(int type, RsService service) + { + services.put(type, service); + } +} diff --git a/app/src/main/java/io/xeres/app/xrs/service/RsServiceType.java b/app/src/main/java/io/xeres/app/xrs/service/RsServiceType.java new file mode 100644 index 000000000..751de1caf --- /dev/null +++ b/app/src/main/java/io/xeres/app/xrs/service/RsServiceType.java @@ -0,0 +1,119 @@ +/* + * Copyright (c) 2019-2021 by David Gerber - https://zapek.com + * + * This file is part of Xeres. + * + * Xeres is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Xeres is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Xeres. If not, see . + */ + +package io.xeres.app.xrs.service; + +public enum RsServiceType +{ + NONE(0, null, 0, 0, 0, 0), + GOSSIP_DISCOVERY(0x11, "disc", 1, 0, 1, 0), + CHAT(0x12, "chat", 1, 0, 1, 0), + MSG(0x13, "msg", 1, 0, 1, 0), + TURTLE(0x14, "turtle", 1, 0, 1, 0), + TUNNEL(0x15, null, 1, 0, 1, 0), + HEARTBEAT(0x16, "heartbeat", 1, 0, 1, 0), + FILE_TRANSFER(0x17, "ft", 1, 0, 1, 0), + GROUTER(0x18, "Global Router", 1, 0, 1, 0), + FILE_DATABASE(0x19, "file_database", 1, 0, 1, 0), + SERVICEINFO(0x20, "serviceinfo", 1, 0, 1, 0), + BANDWIDTH_CONTROL(0x21, "bandwidth_ctrl", 1, 0, 1, 0), + MAIL(0x22, null, 1, 0, 1, 0), + DIRECT_MAIL(0x23, "msgdirect", 1, 0, 1, 0), + DISTANT_MAIL(0x24, null, 1, 0, 1, 0), + GWEMAIL_MAIL(0x25, null, 1, 0, 1, 0), + SERVICE_CONTROL(0x26, null, 1, 0, 1, 0), + DISTANT_CHAT(0x27, null, 1, 0, 1, 0), + GXS_TUNNEL(0x28, "GxsTunnels", 1, 0, 1, 0), + BANLIST(0x101, "banlist", 1, 0, 1, 0), + STATUS(0x102, "status", 1, 0, 1, 0), + NXS(0x200, null, 1, 0, 1, 0), + GXSID(0x211, "gxsid", 1, 0, 1, 0), + PHOTO(0x212, "gxsphoto", 1, 0, 1, 0), + WIKI(0x213, "gxswiki", 1, 0, 1, 0), + WIRE(0x214, "gxswire", 1, 0, 1, 0), + FORUMS(0x215, "gxsforums", 1, 0, 1, 0), + POSTED(0x216, "gxsposted", 1, 0, 1, 0), + CHANNELS(0x217, "gxschannels", 1, 0, 1, 0), + GXSCIRCLE(0x218, "gxscircle", 1, 0, 1, 0), + REPUTATION(0x219, "gxsreputation", 1, 0, 1, 0), + GXS_RECOGN(0x220, null, 1, 0, 1, 0), + GXS_TRANS(0x230, "GXS Mails", 0, 1, 0, 1), + JSONAPI(0x240, null, 1, 0, 1, 0), + FORUMS_CONFIG(0x315, null, 1, 0, 1, 0), + POSTED_CONFIG(0x316, null, 1, 0, 1, 0), + CHANNELS_CONFIG(0x317, null, 1, 0, 1, 0), + RTT(0x1011, "rtt", 1, 0, 1, 0), + // plugins + PLUGIN_ARADO_ID(0x2001, null, 1, 0, 1, 0), + PLUGIN_QCHESS_ID(0x2002, "RetroChess", 1, 0, 1, 0), + PLUGIN_FEEDREADER(0x2003, "FEEDREADER", 1, 0, 1, 0), + PLUGIN_VOIP(0xA021, "VOIP", 1, 0, 1, 0), + // packet slicing + PACKET_SLICING_PROBE(0xAABB, "SlicingProbe", 1, 0, 1, 0), + // Nabu's experimental services + PLUGIN_ZERORESERVE(0xBEEF, null, 1, 0, 1, 0), + PLUGIN_FIDO_GW(0xF1D0, null, 1, 0, 1, 0); + + private final int type; + private final String name; + private final short versionMajor; + private final short versionMinor; + private final short minVersionMajor; + private final short minVersionMinor; + + RsServiceType(int type, String name, int versionMajor, int versionMinor, int minVersionMajor, int minVersionMinor) + { + this.type = type; + this.name = name; + this.versionMajor = (short) versionMajor; + this.versionMinor = (short) versionMinor; + this.minVersionMajor = (short) minVersionMajor; + this.minVersionMinor = (short) minVersionMinor; + } + + public int getType() + { + return type; + } + + public String getName() + { + return name; + } + + public short getVersionMajor() + { + return versionMajor; + } + + public short getVersionMinor() + { + return versionMinor; + } + + public short getMinVersionMajor() + { + return minVersionMajor; + } + + public short getMinVersionMinor() + { + return minVersionMinor; + } +} diff --git a/app/src/main/java/io/xeres/app/xrs/service/chat/ChatFlags.java b/app/src/main/java/io/xeres/app/xrs/service/chat/ChatFlags.java new file mode 100644 index 000000000..aa3fd341c --- /dev/null +++ b/app/src/main/java/io/xeres/app/xrs/service/chat/ChatFlags.java @@ -0,0 +1,38 @@ +/* + * Copyright (c) 2019-2021 by David Gerber - https://zapek.com + * + * This file is part of Xeres. + * + * Xeres is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Xeres is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Xeres. If not, see . + */ + +package io.xeres.app.xrs.service.chat; + +public enum ChatFlags +{ + PRIVATE, + REQUEST_AVATAR, // set when requesting an avatar, the message must be empty and set to private too + CONTAINS_AVATAR, // not used anymore + AVATAR_AVAILABLE, // set if we changed our avatar + CUSTOM_STATE, // used for ChatStatusItem + PUBLIC, + REQUEST_CUSTOM_STATE, // used for ChatStatusItem + CUSTOM_STATE_AVAILABLE, // used for ChatStatusItem + PARTIAL_MESSAGE, + LOBBY, // XXX: might not be needed because we have a ChatRoomMessageItem + CLOSING_DISTANT_CONNECTION, + ACK_DISTANT_CONNECTION, + KEEP_ALIVE, + CONNECTION_REFUSED +} diff --git a/app/src/main/java/io/xeres/app/xrs/service/chat/ChatRoom.java b/app/src/main/java/io/xeres/app/xrs/service/chat/ChatRoom.java new file mode 100644 index 000000000..37645e0cf --- /dev/null +++ b/app/src/main/java/io/xeres/app/xrs/service/chat/ChatRoom.java @@ -0,0 +1,244 @@ +/* + * Copyright (c) 2019-2021 by David Gerber - https://zapek.com + * + * This file is part of Xeres. + * + * Xeres is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Xeres is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Xeres. If not, see . + */ + +package io.xeres.app.xrs.service.chat; + +import io.xeres.app.net.peer.PeerConnection; +import io.xeres.app.xrs.service.chat.item.VisibleChatRoomInfo; +import io.xeres.common.id.GxsId; +import io.xeres.common.id.Id; +import io.xeres.common.id.LocationId; +import io.xeres.common.message.chat.RoomInfo; +import io.xeres.common.message.chat.RoomType; + +import java.time.Instant; +import java.util.EnumSet; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; + +public class ChatRoom +{ + private final long id; + private final String name; + private final String topic; + private final Set participatingPeers = ConcurrentHashMap.newKeySet(); + private GxsId gxsId; // signing entity + private final Map gxsIds = new ConcurrentHashMap<>(); // non direct friends who are participating + private final int userCount; + private Instant lastActivity; + private final RoomType type; + private final boolean signed; + + private final MessageCache messageCache = new MessageCache(); + private LocationId virtualPeerId; // XXX: check if we need that... + private int connectionChallengeCount; + private Instant lastConnectionChallenge = Instant.EPOCH; + private boolean joinedRoomPacketSent; + private Instant lastKeepAlivePacket = Instant.EPOCH; + private final Set previouslyKnownLocations = ConcurrentHashMap.newKeySet(); + + public ChatRoom(long id, String name, String topic, RoomType type, int userCount, boolean isSigned) + { + this.id = id; + this.name = name; + this.topic = topic; + this.type = type; + this.userCount = userCount; // XXX: use that if available, other gxsId.size() which is more precise + this.signed = isSigned; + } + + /** + * Get as a RoomInfo structure, used for displaying in the UI + * + * @return a RoomInfo + */ + public RoomInfo getAsRoomInfo() + { + return new RoomInfo( + id, + name, + type, + topic, + userCount, // XXX: use a getUserCount() which choses what to get + signed + ); + } + + /** + * Get as a VisibleChatRoomInfo, used for serialization as RS protocol + * + * @return a VisibleChatRoomInfo + */ + public VisibleChatRoomInfo getAsVisibleChatRoomInfo() + { + return new VisibleChatRoomInfo( + id, + name, + topic, + userCount, // XXX: ditto + getRoomFlags() + ); + } + + public long getId() + { + return id; + } + + public String getName() + { + return name; + } + + public String getTopic() + { + return topic; + } + + public Set getParticipatingPeers() + { + return participatingPeers; + } + + public void addParticipatingPeer(PeerConnection peerConnection) + { + participatingPeers.add(peerConnection); + } + + public void removeParticipatingPeer(PeerConnection peerConnection) + { + participatingPeers.remove(peerConnection); + } + + public GxsId getGxsId() + { + return gxsId; + } + + public Map getGxsIds() + { + return gxsIds; + } + + public Instant getLastActivity() + { + return lastActivity; + } + + public void updateActivity() + { + lastActivity = Instant.now(); + } + + public LocationId getVirtualPeerId() + { + return virtualPeerId; + } + + public int getConnectionChallengeCount() + { + return connectionChallengeCount; + } + + public int getConnectionChallengeCountAndIncrease() + { + return connectionChallengeCount++; + } + + public void resetConnectionChallengeCount() + { + connectionChallengeCount = 0; + this.lastConnectionChallenge = Instant.now(); + } + + public Instant getLastConnectionChallenge() + { + return lastConnectionChallenge; + } + + // XXX: what is that used for? + public boolean isJoinedRoomPacketSent() + { + return joinedRoomPacketSent; + } + + public void setLastKeepAlivePacket(Instant lastKeepAlivePacket) + { + this.lastKeepAlivePacket = lastKeepAlivePacket; + } + + public Instant getLastKeepAlivePacket() + { + return lastKeepAlivePacket; + } + + public Set getPreviouslyKnownLocations() + { + return previouslyKnownLocations; + } + + public boolean isPublic() + { + return type == RoomType.PUBLIC; + } + + public boolean isSigned() + { + return signed; + } + + public long getNewMessageId() + { + return messageCache.getNewMessageId(); + } + + public void incrementConnectionChallengeCount() + { + connectionChallengeCount++; + } + + public MessageCache getMessageCache() + { + return messageCache; + } + + public Set getRoomFlags() + { + var roomFlags = EnumSet.noneOf(RoomFlags.class); + if (type == RoomType.PUBLIC) + { + roomFlags.add(RoomFlags.PUBLIC); + } + if (signed) + { + roomFlags.add(RoomFlags.PGP_SIGNED); + } + return roomFlags; + } + + @Override + public String toString() + { + return "ChatRoom{" + + "id=" + Id.toStringLowerCase(id) + + ", name='" + name + '\'' + + '}'; + } +} diff --git a/app/src/main/java/io/xeres/app/xrs/service/chat/ChatService.java b/app/src/main/java/io/xeres/app/xrs/service/chat/ChatService.java new file mode 100644 index 000000000..d0b4e1216 --- /dev/null +++ b/app/src/main/java/io/xeres/app/xrs/service/chat/ChatService.java @@ -0,0 +1,838 @@ +/* + * Copyright (c) 2019-2021 by David Gerber - https://zapek.com + * + * This file is part of Xeres. + * + * Xeres is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Xeres is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Xeres. If not, see . + */ + +package io.xeres.app.xrs.service.chat; + +import io.xeres.app.database.DatabaseSession; +import io.xeres.app.database.DatabaseSessionManager; +import io.xeres.app.net.peer.PeerConnection; +import io.xeres.app.net.peer.PeerConnectionManager; +import io.xeres.app.service.ChatRoomService; +import io.xeres.app.service.IdentityService; +import io.xeres.app.service.LocationService; +import io.xeres.app.xrs.common.Signature; +import io.xeres.app.xrs.item.Item; +import io.xeres.app.xrs.service.RsService; +import io.xeres.app.xrs.service.RsServiceInitPriority; +import io.xeres.app.xrs.service.RsServiceType; +import io.xeres.app.xrs.service.chat.item.*; +import io.xeres.common.id.GxsId; +import io.xeres.common.id.Id; +import io.xeres.common.id.LocationId; +import io.xeres.common.message.chat.ChatRoomListMessage; +import io.xeres.common.message.chat.ChatRoomMessage; +import io.xeres.common.message.chat.PrivateChatMessage; +import io.xeres.common.message.chat.RoomType; +import io.xeres.common.util.NoSuppressedRunnable; +import org.jsoup.Jsoup; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.core.env.Environment; +import org.springframework.stereotype.Component; +import org.springframework.transaction.annotation.Transactional; + +import java.time.Duration; +import java.time.Instant; +import java.util.EnumSet; +import java.util.Map; +import java.util.Objects; +import java.util.Set; +import java.util.concurrent.*; + +import static io.xeres.app.xrs.service.RsServiceType.CHAT; +import static io.xeres.common.message.MessageType.*; +import static io.xeres.common.rest.PathConfig.CHAT_PATH; +import static java.util.Map.entry; + +// XXX: check if everything is thread safe (the lists are but are the operations ok?) +@Component +public class ChatService extends RsService +{ + private static final Logger log = LoggerFactory.getLogger(ChatService.class); + + private final Map chatRooms = new ConcurrentHashMap<>(); + private final Map availableChatRooms = new ConcurrentHashMap<>(); + + /** + * Time between housekeeping runs to cleanup the message cache and so on. + */ + private static final Duration HOUSEKEEPING_DELAY = Duration.ofSeconds(10); + + /** + * Maximum time to keep message records. + */ + private static final Duration KEEP_MESSAGE_RECORD_MAX = Duration.ofMinutes(20); + + /** + * Maximum of chat rooms accepted by a peer. + * XXX: should be incremented one day + */ + private static final int CHATROOM_LIST_MAX = 50; + + /** + * Time after which a keep alive packet is sent. + */ + private static final Duration KEEPALIVE_DELAY = Duration.ofMinutes(2); + + /** + * Minimum time between connection challenges. + */ + private static final Duration CONNECTION_CHALLENGE_MIN_DELAY = Duration.ofSeconds(15); + + /** + * Minimum number of connection challenge counts before one + * can be sent. + */ + private static final int CONNECTION_CHALLENGE_COUNT_MIN = 20; + + /** + * Maximum time difference allowed for messages in the past (this doesn't + * account for KEEP_MESSAGE_RECORD_MAX for the total). + */ + private static final Duration TIME_DRIFT_PAST_MAX = Duration.ofSeconds(100); + + /** + * Maximum time difference allowed for messages in the future. + */ + private static final Duration TIME_DRIFT_FUTURE_MAX = Duration.ofMinutes(10); + + /** + * Content sent with a typing notification. Note that Retroshare displays + * the text directly. + */ + private static final String MESSAGE_TYPING_CONTENT = "is typing..."; + + private enum Invitation + { + PLAIN, + FROM_CHALLENGE + } + + private final LocationService locationService; + private final PeerConnectionManager peerConnectionManager; + private final IdentityService identityService; + private final ChatRoomService chatRoomService; + private final DatabaseSessionManager databaseSessionManager; + + private ScheduledExecutorService executorService; + + public ChatService(Environment environment, PeerConnectionManager peerConnectionManager, LocationService locationService, PeerConnectionManager peerConnectionManager1, IdentityService identityService, ChatRoomService chatRoomService, DatabaseSessionManager databaseSessionManager) + { + super(environment, peerConnectionManager); + this.locationService = locationService; + this.peerConnectionManager = peerConnectionManager1; + this.identityService = identityService; + this.chatRoomService = chatRoomService; + this.databaseSessionManager = databaseSessionManager; + } + + @Override + public RsServiceType getServiceType() + { + return CHAT; + } + + @Override + public Map, Integer> getSupportedItems() + { + return Map.ofEntries( + entry(ChatMessageItem.class, 1), + entry(ChatAvatarItem.class, 3), + entry(ChatStatusItem.class, 4), + entry(PrivateChatMessageConfigItem.class, 5), + entry(ChatRoomConnectChallengeItem.class, 9), + entry(ChatRoomUnsubscribeItem.class, 10), + entry(ChatRoomListRequestItem.class, 13), + entry(ChatRoomConfigItem.class, 21), + entry(ChatRoomMessageItem.class, 23), + entry(ChatRoomEventItem.class, 24), + entry(ChatRoomListItem.class, 25), + entry(ChatRoomInviteItem.class, 27), + entry(PrivateOutgoingMapItem.class, 28), + entry(SubscribedChatRoomConfigItem.class, 29) + ); + } + + @Override + public RsServiceInitPriority getInitPriority() + { + return RsServiceInitPriority.LOW; + } + + @Override + @SuppressWarnings("java:S1905") + public void initialize() + { + executorService = Executors.newSingleThreadScheduledExecutor(); + + executorService.scheduleAtFixedRate((NoSuppressedRunnable) this::manageChatRooms + , HOUSEKEEPING_DELAY.toSeconds(), + HOUSEKEEPING_DELAY.toSeconds(), + TimeUnit.SECONDS + ); + } + + @Override + public void shutdown() + { + chatRooms.forEach((id, chatRoom) -> sendChatRoomEvent(chatRoom, ChatRoomEvent.PEER_LEFT)); + // XXX: check if that works, otherwise add some delays... + } + + @Override + public void cleanup() + { + executorService.shutdownNow(); + } + + @Override + public void initialize(PeerConnection peerConnection) + { + peerConnection.scheduleAtFixedRate( + () -> askForNearbyChatRooms(peerConnection) + , 0, + 120, + TimeUnit.SECONDS + ); + } + + private void manageChatRooms() + { + chatRooms.forEach((roomId, chatRoom) -> { + + // Remove old messages + chatRoom.getMessageCache().purge(); + + // Remove inactive gxsIds + // XXX: implement... + + // XXX: send joined_lobby if participating friend connected... maybe do it when he actually connects! + + sendKeepAliveIfNeeded(chatRoom); + + sendConnectionChallengeIfNeeded(chatRoom); + }); + + // XXX: remove outdated rooms visible lobbies + + // XXX: add the rest of the handling... + // XXX: note that some events can be handled better by being sent directly do the peer upon action (eg. join is already on auto subscribe) + // XXX: also. maybe it's better to have several scheduledAtFixedRate() instead of doing each 10 seconds + checks and storage... with a schelder, while it does do "pulses", the only case where it's more + // efficient to do it the RS way is when we join a channel in the middle of a session, which happens rarely + } + + private void askForNearbyChatRooms(PeerConnection peerConnection) + { + writeItem(peerConnection, new ChatRoomListRequestItem()); + } + + private void sendKeepAliveIfNeeded(ChatRoom chatRoom) + { + var now = Instant.now(); + + if (Duration.between(chatRoom.getLastKeepAlivePacket(), now).compareTo(KEEPALIVE_DELAY) > 0) + { + sendChatRoomEvent(chatRoom, ChatRoomEvent.KEEP_ALIVE); + chatRoom.setLastKeepAlivePacket(now); + } + } + + private void sendConnectionChallengeIfNeeded(ChatRoom chatRoom) + { + if (chatRoom.getConnectionChallengeCountAndIncrease() > CONNECTION_CHALLENGE_COUNT_MIN && + Duration.between(chatRoom.getLastConnectionChallenge(), Instant.now()).compareTo(CONNECTION_CHALLENGE_MIN_DELAY) > 0) + { + chatRoom.resetConnectionChallengeCount(); + + long recentMessage = chatRoom.getMessageCache().getRecentMessage(); + if (recentMessage == 0) + { + log.debug("No message in cache to send connection challenge. Not enough activity?"); + return; + } + + // Send connection challenge to all connected friends + peerConnectionManager.doForAllPeers(peerConnection -> writeItem(peerConnection, new ChatRoomConnectChallengeItem(peerConnection.getLocation().getLocationId(), chatRoom.getId(), recentMessage)), this); + } + } + + @Override + public void handleItem(PeerConnection peerConnection, Item item) + { + if (item instanceof ChatRoomListRequestItem) + { + handleChatRoomListRequestItem(peerConnection); + } + else if (item instanceof ChatRoomListItem chatRoomListItem) + { + handleChatRoomListItem(peerConnection, chatRoomListItem); + } + else if (item instanceof ChatMessageItem chatMessageItem) + { + handleChatMessageItem(peerConnection, chatMessageItem); + } + else if (item instanceof ChatRoomMessageItem chatRoomMessageItem) + { + handleChatRoomMessageItem(peerConnection, chatRoomMessageItem); + } + else if (item instanceof ChatStatusItem chatStatusItem) + { + handleChatStatusItem(peerConnection, chatStatusItem); + } + else if (item instanceof ChatRoomInviteItem chatRoomInviteItem) + { + handleChatRoomInviteItem(peerConnection, chatRoomInviteItem); + } + else if (item instanceof ChatRoomEventItem chatRoomEventItem) + { + handleChatRoomEventItem(peerConnection, chatRoomEventItem); + } + else if (item instanceof ChatRoomConnectChallengeItem chatRoomConnectChallengeItem) + { + handleChatRoomConnectChallengeItem(peerConnection, chatRoomConnectChallengeItem); + } + else if (item instanceof ChatRoomUnsubscribeItem chatRoomUnsubscribeItem) + { + handleChatRoomUnsubscribeItem(peerConnection, chatRoomUnsubscribeItem); + } + } + + private void handleChatRoomListItem(PeerConnection peerConnection, ChatRoomListItem item) + { + log.debug("Received chat room list from {}", peerConnection); + if (item.getChatRooms().size() > CHATROOM_LIST_MAX) + { + log.warn("Location {} is sending a chat room list of {} items, which is bigger than the allowed {}", peerConnection, item.getChatRooms().size(), CHATROOM_LIST_MAX); + } + var roomListMessage = new ChatRoomListMessage(); + item.getChatRooms().stream() + .limit(CHATROOM_LIST_MAX) + .forEach(visibleRoom -> { + var chatRoom = new ChatRoom( + visibleRoom.getId(), + visibleRoom.getName(), + visibleRoom.getTopic(), + visibleRoom.getFlags().contains(RoomFlags.PUBLIC) ? RoomType.PUBLIC : RoomType.PRIVATE, + visibleRoom.getCount(), + visibleRoom.getFlags().contains(RoomFlags.PGP_SIGNED) + ); + chatRoom.addParticipatingPeer(peerConnection); + roomListMessage.add(chatRoom.getAsRoomInfo()); + availableChatRooms.put(chatRoom.getId(), chatRoom); + }); + + peerConnectionManager.sendToSubscriptions(CHAT_PATH, CHAT_ROOM_LIST, roomListMessage); + + chatRoomService.getAllChatRoomsPendingToSubscribe().forEach(chatRoom -> joinChatRoom(chatRoom.getRoomId())); + // then just send a message with the subscribed/unsubscribed lists for the UI. don't forget to protect the calls + } + + private void handleChatRoomListRequestItem(PeerConnection peerConnection) + { + var chatRoomListItem = new ChatRoomListItem(chatRooms.values().stream() + .filter(chatRoom -> chatRoom.isPublic() + || chatRoom.getPreviouslyKnownLocations().contains(peerConnection.getLocation().getLocationId()) + || chatRoom.getParticipatingPeers().contains(peerConnection)) + .map(ChatRoom::getAsVisibleChatRoomInfo) + .toList()); + + writeItem(peerConnection, chatRoomListItem); + } + + private void handleChatRoomMessageItem(PeerConnection peerConnection, ChatRoomMessageItem item) + { + if (!validateExpiration(item.getSendTime())) + { + log.warn("Received message from peer {} failed time validation, dropping", peerConnection); + } + + if (isBanned(item.getSignature().getGxsId())) + { + log.debug("Dropping message from banned entity {}", item.getSignature().getGxsId()); + return; + } + + if (!validateBounceSignature(item)) + { + log.error("Received invalid message from peer {}, dropping", peerConnection); + return; + } + + // XXX: add routing clue (ie. best peer for channel) + + var chatRoom = chatRooms.get(item.getRoomId()); + + if (chatRoom == null) + { + log.debug("Received message from peer {} but we're not subscribed to chat room id {}, dropping", peerConnection, log.isDebugEnabled() ? Id.toStringLowerCase(item.getRoomId()) : null); + return; + } + + if (!bounce(item, peerConnection)) + { + return; + } + + // And display the message for us + var chatRoomMessage = new ChatRoomMessage(item.getSenderNickname(), Jsoup.parse(item.getMessage()).text()); + peerConnectionManager.sendToSubscriptions(CHAT_PATH, CHAT_ROOM_MESSAGE, item.getRoomId(), chatRoomMessage); + + chatRoom.incrementConnectionChallengeCount(); // XXX: this allows to find out when to send challenges. do that (where?) + } + + private void handleChatRoomEventItem(PeerConnection peerConnection, ChatRoomEventItem item) + { + if (!chatRooms.containsKey(item.getRoomId())) + { + log.error("We're not subscribed to chat room id {}, dropping event", log.isErrorEnabled() ? Id.toStringLowerCase(item.getRoomId()) : null); + return; + } + + if (isBanned(item.getSignature().getGxsId())) + { + log.debug("Dropping event from banned entity {}", item.getSignature().getGxsId()); + return; + } + + if (!validateBounceSignature(item)) + { + log.error("Received invalid message from peer {}, dropping", peerConnection); + return; + } + + // XXX: check if it's for signed lobby + + // XXX: addTimeShiftStatistics()... why isn't this done for messages as well? it just displays a warning anyway (and it's disabled in RS so it does nothing) + if (!validateExpiration(item.getSendTime())) + { + log.warn("Received message from peer {} failed time validation, dropping", peerConnection); + } + + // XXX: add routing clue + + if (!bounce(item, peerConnection)) + { + return; + } + + if (item.getEventType() == ChatRoomEvent.PEER_LEFT.getCode()) + { + // XXX: remove nickname from the list + } + else if (item.getEventType() == ChatRoomEvent.PEER_JOINED.getCode()) + { + // XXX: add nickname to the list + // XXX: send a keep alive event to the participant so that he knows we are in the room (RS sends to everyone but that's lame) + } + else if (item.getEventType() == ChatRoomEvent.KEEP_ALIVE.getCode()) + { + // XXX: not sure what to do... refresh the time of the gxsid in the room? + } + } + + private void handleChatRoomUnsubscribeItem(PeerConnection peerConnection, ChatRoomUnsubscribeItem item) + { + var chatRoom = chatRooms.get(item.getRoomId()); + if (chatRoom == null) + { + log.error("Cannot unsubscribe peer from chat room {} as we're not in it", log.isErrorEnabled() ? Id.toStringLowerCase(item.getRoomId()) : null); + return; + } + + chatRoom.removeParticipatingPeer(peerConnection); + + // XXX: RS has some "previously_known_peers"... see if it's useful + } + + private void handleChatRoomInviteItem(PeerConnection peerConnection, ChatRoomInviteItem item) + { + log.debug("Received invite from {} to room {}", peerConnection, item.getRoomName()); + + var chatRoom = chatRooms.get(item.getRoomId()); + if (chatRoom != null) + { + if (!item.isConnectionChallenge() && (chatRoom.isPublic() != item.isPublic() || chatRoom.isSigned() != item.isSigned())) + { + log.debug("Not a matching item"); + return; + } + + log.debug("Adding peer {} to chat room {}", peerConnection, chatRoom); + + chatRoom.addParticipatingPeer(peerConnection); + } + else + { + if (!item.isConnectionChallenge()) + { + // XXX: prompt the user for a lobby invite, store it somewhere if it's accepted to be able to join + log.debug("Is a lobby invite to join a new room"); + } + } + } + + private void handleChatStatusItem(PeerConnection peerConnection, ChatStatusItem item) + { + // There's a whole protocol with the flags (REQUEST_CUSTOM_STATE, CUSTOM_STATE and CUSTOM_STATE_AVAILABLE) + // to send and request states but it seems all RS does is send the typing state every + // 5 seconds while the user is typing. + log.debug("Got status item from peer {} with status string: {}", peerConnection, item.getStatus()); + if (MESSAGE_TYPING_CONTENT.equals(item.getStatus())) + { + var privateChatMessage = new PrivateChatMessage(); + peerConnectionManager.sendToSubscriptions(CHAT_PATH, CHAT_TYPING_NOTIFICATION, peerConnection.getLocation().getLocationId(), privateChatMessage); + } + else + { + log.warn("Unknown status item from peer {}, status: {}, flags: {}", peerConnection, item.getStatus(), item.getFlags()); + } + } + + private void handleChatMessageItem(PeerConnection peerConnection, ChatMessageItem item) + { + if (item.isPrivate() && !item.isAvatarRequest()) // XXX: handle avatars later + { + var privateChatMessage = new PrivateChatMessage(Jsoup.parse(item.getMessage()).text()); // XXX: jsoup also supports removing against a whitelist only (eg. if we want to keep , , etc... https://jsoup.org/cookbook/cleaning-html/whitelist-sanitizer + peerConnectionManager.sendToSubscriptions(CHAT_PATH, CHAT_PRIVATE_MESSAGE, peerConnection.getLocation().getLocationId(), privateChatMessage); + } + } + + private void handleChatRoomConnectChallengeItem(PeerConnection peerConnection, ChatRoomConnectChallengeItem item) + { + var locationId = peerConnection.getLocation().getLocationId(); + + for (ChatRoom chatRoom : chatRooms.values()) + { + if (chatRoom.getMessageCache().hasConnectionChallenge(locationId, chatRoom.getId(), item.getChallengeCode())) + { + log.debug("Challenge accepted for chatroom {}, sending connection request to peer {}", chatRoom, peerConnection); + chatRoom.addParticipatingPeer(peerConnection); + invitePeerToChatRoom(peerConnection, chatRoom, Invitation.FROM_CHALLENGE); + return; + } + } + } + + private void invitePeerToChatRoom(PeerConnection peerConnection, ChatRoom chatRoom, Invitation invitation) + { + var item = new ChatRoomInviteItem( + chatRoom.getId(), + chatRoom.getName(), + chatRoom.getTopic(), + invitation == Invitation.FROM_CHALLENGE ? EnumSet.of(RoomFlags.CHALLENGE) : chatRoom.getRoomFlags()); + peerConnectionManager.writeItem(peerConnection, item, this); + } + + private void signalChatRoomLeave(PeerConnection peerConnection, ChatRoom chatRoom) + { + var item = new ChatRoomUnsubscribeItem(chatRoom.getId()); + peerConnectionManager.writeItem(peerConnection, item, this); + } + + private void initializeBounce(ChatRoom chatRoom, ChatRoomBounce bounce) + { + try (var session = new DatabaseSession(databaseSessionManager)) + { + var ownIdentity = identityService.getOwnIdentity(); + + bounce.setRoomId(chatRoom.getId()); + bounce.setMessageId(chatRoom.getNewMessageId()); + bounce.setSenderNickname(ownIdentity.getGxsIdGroupItem().getName()); // XXX: we should use the identity in chatRoom.getGxsId() once we have multiple identities support done properly + + byte[] signature = identityService.signData(ownIdentity, getBounceData(bounce)); + + bounce.setSignature(new Signature(ownIdentity.getGxsIdGroupItem().getGxsId(), signature)); + } + } + + private boolean bounce(ChatRoomBounce bounce) + { + return bounce(bounce, null); + } + + private boolean bounce(ChatRoomBounce bounce, PeerConnection peerConnection) + { + var chatRoom = chatRooms.get(bounce.getRoomId()); + if (chatRoom == null) + { + log.error("Can't send to chat room {}, we're not subscribed to it", log.isErrorEnabled() ? Id.toStringLowerCase(bounce.getRoomId()) : null); + return false; + } + + if (peerConnection != null) + { + chatRoom.addParticipatingPeer(peerConnection); // If we didn't receive the list yet it means he's participating still + } + + if (chatRoom.getMessageCache().exists(bounce.getMessageId())) + { + log.warn("Message id {} already received, dropping", bounce.getMessageId()); + chatRoom.getMessageCache().update(bounce.getMessageId()); // prevent echoes + return false; + } + + chatRoom.getMessageCache().add(bounce.getMessageId()); + chatRoom.updateActivity(); + + // XXX: check for antiflood + + // Send to everyone except the originating peer + chatRoom.getParticipatingPeers().forEach(peer -> { + if (!Objects.equals(peer, peerConnection)) + { + writeItem(peer, bounce); + } + }); + + chatRoom.incrementConnectionChallengeCount(); + + return true; + } + + private boolean validateBounceSignature(ChatRoomBounce bounce) + { + // XXX: implement signature validation. needs public key from the gxsid on the peer. there must be a way to request the peer's key + return true; + } + + private boolean isBanned(GxsId gxsId) + { + // XXX: implement by using the reputation level + return false; + } + + private byte[] getBounceData(ChatRoomBounce chatRoomBounce) + { + var buffer = peerConnectionManager.serializeItemForSignature(chatRoomBounce, this); + var data = new byte[buffer.writerIndex()]; + buffer.getBytes(0, data); + buffer.release(); + return data; + } + + /** + * Checks if a message is well within our own time. + * + * @param sendTime the time the message was sent at, in seconds from 1970-01-01 UTC + * @return true if within bounds + */ + private boolean validateExpiration(int sendTime) + { + var now = Instant.now(); + if (sendTime < now.getEpochSecond() + TIME_DRIFT_PAST_MAX.toSeconds() - KEEP_MESSAGE_RECORD_MAX.toSeconds()) + { + return false; + } + + if (sendTime > now.getEpochSecond() + TIME_DRIFT_FUTURE_MAX.toSeconds()) + { + return false; + } + return true; + } + + /** + * Sends a broadcast message to all connected peers. + * + * @param message the message + */ + public void sendBroadcastMessage(String message) + { + var chatMessageItem = new ChatMessageItem(message, EnumSet.of(ChatFlags.PUBLIC)); + peerConnectionManager.doForAllPeers(peerConnection -> writeItem(peerConnection, chatMessageItem), this); + } + + /** + * Sends a private message to a peer. + * + * @param locationId the location id + * @param message the message + */ + @Transactional(readOnly = true) + public void sendPrivateMessage(LocationId locationId, String message) + { + var location = locationService.findLocationById(locationId).orElseThrow(); + writeItem(location, new ChatMessageItem(message, EnumSet.of(ChatFlags.PRIVATE))); + } + + /** + * Sends a typing notification for private messages to a peer. + * + * @param locationId the location id + */ + @Transactional(readOnly = true) + public void sendPrivateTypingNotification(LocationId locationId) + { + var location = locationService.findLocationById(locationId).orElseThrow(); + writeItem(location, new ChatStatusItem(MESSAGE_TYPING_CONTENT, EnumSet.of(ChatFlags.PRIVATE))); + } + + /** + * Sets the status message (the one appearing at the top of the profile peer, ie. "I'm eating", "Gone for a walk", etc...). + * + * @param message the status message + */ + public void setStatusMessage(String message) + { + peerConnectionManager.doForAllPeers(peerConnection -> writeItem(peerConnection, new ChatStatusItem(message, EnumSet.of(ChatFlags.CUSTOM_STATE))), this); + } + + /** + * Sends a message to a chat room. + * + * @param chatRoomId the id of the chat room + * @param message the message + */ + public void sendChatRoomMessage(long chatRoomId, String message) + { + var chatRoomMessageItem = new ChatRoomMessageItem(message); + + var chatRoom = chatRooms.get(chatRoomId); + if (chatRoom == null) + { + log.warn("Chatroom {} doesn't exist. Not sending the message.", log.isWarnEnabled() ? Id.toStringLowerCase(chatRoomId) : null); + return; + } + + initializeBounce(chatRoom, chatRoomMessageItem); + bounce(chatRoomMessageItem); + } + + public void sendChatRoomTypingNotification(long chatRoomId) + { + var chatRoom = chatRooms.get(chatRoomId); + if (chatRoom == null) + { + log.warn("Chatroom {} doesn't exist. Not sending the typing notification.", log.isWarnEnabled() ? Id.toStringLowerCase(chatRoomId) : null); + return; + } + sendChatRoomEvent(chatRoom, ChatRoomEvent.PEER_STATUS, MESSAGE_TYPING_CONTENT); + } + + /** + * Joins a chat room. + * + * @param chatRoomId the id of the chat room + */ + public void joinChatRoom(long chatRoomId) + { + log.debug("Joining chat room {}", log.isDebugEnabled() ? Id.toStringLowerCase(chatRoomId) : null); + if (chatRooms.containsKey(chatRoomId)) + { + log.debug("Already in the chatroom"); + return; + } + + var chatRoom = availableChatRooms.get(chatRoomId); + if (chatRoom == null) + { + log.warn("Chatroom {} doesn't exist, can't join.", log.isWarnEnabled() ? Id.toStringLowerCase(chatRoomId) : null); + return; + } + chatRooms.put(chatRoomId, chatRoom); + + chatRoomService.subscribeToChatRoomAndJoin(chatRoom, identityService.getOwnIdentity()); // XXX: allow multiple identities + + chatRoom.getParticipatingPeers().forEach(peer -> invitePeerToChatRoom(peer, chatRoom, Invitation.PLAIN)); + + peerConnectionManager.sendToSubscriptions(CHAT_PATH, CHAT_ROOM_JOIN, chatRoom.getId(), new ChatRoomMessage()); + + sendChatRoomEvent(chatRoom, ChatRoomEvent.PEER_JOINED); // XXX: produces an exception! + } + + /** + * Leaves a chat room. + * + * @param chatRoomId the id of the chat room + */ + public void leaveChatRoom(long chatRoomId) + { + log.debug("Leaving chat room {}", log.isDebugEnabled() ? Id.toStringLowerCase(chatRoomId) : null); + var chatRoomToRemove = chatRooms.get(chatRoomId); + if (chatRoomToRemove == null) + { + log.debug("Can't leave a chatroom we aren't into"); + return; + } + sendChatRoomEvent(chatRoomToRemove, ChatRoomEvent.PEER_LEFT); // XXX: produces an exception! + chatRooms.remove(chatRoomId); + chatRoomService.unsubscribeFromChatRoomAndLeave(chatRoomId, identityService.getOwnIdentity()); // XXX: allow multiple identities + + chatRoomToRemove.getParticipatingPeers().forEach(peer -> signalChatRoomLeave(peer, chatRoomToRemove)); + peerConnectionManager.sendToSubscriptions(CHAT_PATH, CHAT_ROOM_LEAVE, chatRoomToRemove.getId(), new ChatRoomMessage()); + } + + public long createChatRoom(String roomName, String topic, GxsId identity, Set flags) + { + var newChatRoom = new ChatRoom( + createUniqueRoomId(), + roomName, + topic, + RoomType.PUBLIC, // XXX: for now + 1, + false); // XXX: for now + + availableChatRooms.put(newChatRoom.getId(), newChatRoom); + + joinChatRoom(newChatRoom.getId()); + + // XXX: we could invite friends in there... supply a list of friends as parameter + + return newChatRoom.getId(); + } + + private long createUniqueRoomId() + { + long newId; + + do + { + newId = ThreadLocalRandom.current().nextLong(); + } + while (availableChatRooms.containsKey(newId) || chatRooms.containsKey(newId)); + + return newId; + } + + /** + * Send a chat room event to the participating peers. + * + * @param chatRoom the chat room + * @param event the event + */ + private void sendChatRoomEvent(ChatRoom chatRoom, ChatRoomEvent event) + { + sendChatRoomEvent(chatRoom, event, ""); + } + + /** + * Send a chat room event to the participating peers. + * + * @param chatRoom the chat room + * @param event the event + * @param status the status, if empty prefer {@linkplain #sendChatRoomEvent(ChatRoom, ChatRoomEvent) the overloaded alternative} + */ + private void sendChatRoomEvent(ChatRoom chatRoom, ChatRoomEvent event, String status) + { + var chatRoomEvent = new ChatRoomEventItem(event, status); + + initializeBounce(chatRoom, chatRoomEvent); + bounce(chatRoomEvent); + } +} diff --git a/app/src/main/java/io/xeres/app/xrs/service/chat/MessageCache.java b/app/src/main/java/io/xeres/app/xrs/service/chat/MessageCache.java new file mode 100644 index 000000000..412f15edf --- /dev/null +++ b/app/src/main/java/io/xeres/app/xrs/service/chat/MessageCache.java @@ -0,0 +1,137 @@ +/* + * Copyright (c) 2019-2021 by David Gerber - https://zapek.com + * + * This file is part of Xeres. + * + * Xeres is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Xeres is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Xeres. If not, see . + */ + +package io.xeres.app.xrs.service.chat; + +import io.xeres.app.crypto.chatcipher.ChatChallenge; +import io.xeres.common.id.LocationId; + +import java.time.Instant; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ThreadLocalRandom; + +public class MessageCache +{ + private static final int CONNECTION_CHALLENGE_MAX_TIME = 30; // maximum age in seconds a message can be used in a connection challenge + private static final int LIFETIME_MAX = 1200; // maximum age of a message in seconds + + private final Map messages = new ConcurrentHashMap<>(); + + + /** + * Check if a message has been recorded already. If yes, update + * its time to prevent echoes. + * + * @param id the id of the message to check + * @return true if it exists + */ + public boolean exists(long id) + { + return messages.replace(id, (int) Instant.now().getEpochSecond()) != null; + } + + /** + * Add a message id to the cache. + * + * @param id the message id + */ + public void add(long id) + { + messages.put(id, (int) Instant.now().getEpochSecond()); + } + + /** + * Update the time of a message id. + * + * @param id the message id + */ + public void update(long id) + { + add(id); + } + + /** + * Get a new unique message id + * + * @return the message id + */ + public long getNewMessageId() + { + long newId; + + do + { + newId = ThreadLocalRandom.current().nextLong(); + } + while (messages.containsKey(newId)); + + return newId; + } + + /** + * Check if this message cache contains a challenge code. + * + * @param locationId the location id of the peer + * @param chatRoomId the chat room id + * @param challengeCode the challenge code to be matched against + * @return true if challengeCode is in one of a suitable message + */ + public boolean hasConnectionChallenge(LocationId locationId, long chatRoomId, long challengeCode) + { + int now = (int) Instant.now().getEpochSecond(); + + for (Map.Entry message : messages.entrySet()) + { + if (message.getValue() + CONNECTION_CHALLENGE_MAX_TIME + 5 > now && challengeCode == ChatChallenge.code(locationId, chatRoomId, message.getKey())) + { + return true; + } + } + return false; + } + + /** + * Return a recent message id. + * + * @return the message id of a recent message. If there's nothing suitable, return 0 + */ + public long getRecentMessage() + { + int now = (int) Instant.now().getEpochSecond(); + + for (Map.Entry message : messages.entrySet()) + { + if (message.getValue() + CONNECTION_CHALLENGE_MAX_TIME > now) + { + return message.getKey(); + } + } + return 0L; + } + + /** + * Remove all messages older than LIFETIME_MAX seconds. + */ + public void purge() + { + int now = (int) Instant.now().getEpochSecond(); + messages.entrySet().removeIf(entry -> entry.getValue() + LIFETIME_MAX < now); + } +} diff --git a/app/src/main/java/io/xeres/app/xrs/service/chat/RoomFlags.java b/app/src/main/java/io/xeres/app/xrs/service/chat/RoomFlags.java new file mode 100644 index 000000000..4e01a6f55 --- /dev/null +++ b/app/src/main/java/io/xeres/app/xrs/service/chat/RoomFlags.java @@ -0,0 +1,29 @@ +/* + * Copyright (c) 2019-2021 by David Gerber - https://zapek.com + * + * This file is part of Xeres. + * + * Xeres is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Xeres is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Xeres. If not, see . + */ + +package io.xeres.app.xrs.service.chat; + +public enum RoomFlags +{ + AUTO_SUBSCRIBE, + DEPRECATED, + PUBLIC, + CHALLENGE, + PGP_SIGNED +} diff --git a/app/src/main/java/io/xeres/app/xrs/service/chat/item/ChatAvatarItem.java b/app/src/main/java/io/xeres/app/xrs/service/chat/item/ChatAvatarItem.java new file mode 100644 index 000000000..456e80323 --- /dev/null +++ b/app/src/main/java/io/xeres/app/xrs/service/chat/item/ChatAvatarItem.java @@ -0,0 +1,55 @@ +/* + * Copyright (c) 2019-2021 by David Gerber - https://zapek.com + * + * This file is part of Xeres. + * + * Xeres is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Xeres is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Xeres. If not, see . + */ + +package io.xeres.app.xrs.service.chat.item; + +import io.xeres.app.xrs.item.Item; +import io.xeres.app.xrs.serialization.RsSerialized; +import io.xeres.common.id.Id; + +public class ChatAvatarItem extends Item +{ + @RsSerialized + private byte[] imageData; + + @SuppressWarnings("unused") + public ChatAvatarItem() + { + // Required + } + + @Override + public int getPriority() + { + return 2; + } + + public byte[] getImageData() + { + return imageData; + } + + @Override + public String toString() + { + return "ChatAvatarItem{" + + "imageData=" + Id.toString(imageData) + + '}'; + } +} diff --git a/app/src/main/java/io/xeres/app/xrs/service/chat/item/ChatMessageItem.java b/app/src/main/java/io/xeres/app/xrs/service/chat/item/ChatMessageItem.java new file mode 100644 index 000000000..92b6f88c9 --- /dev/null +++ b/app/src/main/java/io/xeres/app/xrs/service/chat/item/ChatMessageItem.java @@ -0,0 +1,98 @@ +/* + * Copyright (c) 2019-2021 by David Gerber - https://zapek.com + * + * This file is part of Xeres. + * + * Xeres is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Xeres is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Xeres. If not, see . + */ + +package io.xeres.app.xrs.service.chat.item; + +import io.xeres.app.xrs.item.Item; +import io.xeres.app.xrs.item.ItemPriority; +import io.xeres.app.xrs.serialization.RsSerialized; +import io.xeres.app.xrs.service.chat.ChatFlags; + +import java.time.Instant; +import java.util.Set; + +import static io.xeres.app.xrs.serialization.TlvType.STR_MSG; +import static io.xeres.app.xrs.service.chat.ChatFlags.PRIVATE; +import static io.xeres.app.xrs.service.chat.ChatFlags.REQUEST_AVATAR; + +public class ChatMessageItem extends Item +{ + @RsSerialized + private Set flags; + + @RsSerialized + private int sendTime; + + @RsSerialized(tlvType = STR_MSG) + private String message; + + @SuppressWarnings("unused") + public ChatMessageItem() + { + // Required + } + + public ChatMessageItem(String message, Set flags) + { + this.message = message; + this.sendTime = (int) Instant.now().getEpochSecond(); + this.flags = flags; + } + + @Override + public int getPriority() + { + return ItemPriority.CHAT.getPriority(); + } + + public Set getFlags() + { + return flags; + } + + public int getSendTime() + { + return sendTime; + } + + public String getMessage() + { + return message; + } + + public boolean isPrivate() + { + return flags.contains(PRIVATE); + } + + public boolean isAvatarRequest() + { + return flags.contains(REQUEST_AVATAR); + } + + @Override + public String toString() + { + return "ChatMessageItem{" + + "flags=" + flags + + ", sendTime=" + sendTime + + ", message='" + message + '\'' + + '}'; + } +} diff --git a/app/src/main/java/io/xeres/app/xrs/service/chat/item/ChatRoomBounce.java b/app/src/main/java/io/xeres/app/xrs/service/chat/item/ChatRoomBounce.java new file mode 100644 index 000000000..89a7d047e --- /dev/null +++ b/app/src/main/java/io/xeres/app/xrs/service/chat/item/ChatRoomBounce.java @@ -0,0 +1,126 @@ +/* + * Copyright (c) 2019-2021 by David Gerber - https://zapek.com + * + * This file is part of Xeres. + * + * Xeres is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Xeres is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Xeres. If not, see . + */ + +package io.xeres.app.xrs.service.chat.item; + +import io.netty.buffer.ByteBuf; +import io.xeres.app.xrs.common.Signature; +import io.xeres.app.xrs.item.Item; +import io.xeres.app.xrs.item.ItemPriority; +import io.xeres.app.xrs.serialization.SerializationFlags; +import io.xeres.app.xrs.serialization.Serializer; +import io.xeres.common.id.Id; + +import java.util.Set; + +import static io.xeres.app.xrs.serialization.TlvType.SIGNATURE; +import static io.xeres.app.xrs.serialization.TlvType.STR_NAME; + +public abstract class ChatRoomBounce extends Item +{ + private long roomId; + private long messageId; + private String senderNickname; + private Signature signature; + + protected ChatRoomBounce() + { + // Needed + } + + @Override + public int getPriority() + { + return ItemPriority.CHAT.getPriority(); + } + + int writeBounceableObject(ByteBuf buf, Set flags) + { + var size = 0; + + size += Serializer.serialize(buf, roomId); + size += Serializer.serialize(buf, messageId); + size += Serializer.serialize(buf, STR_NAME, senderNickname); + + if (!flags.contains(SerializationFlags.SIGNATURE)) + { + size += Serializer.serialize(buf, SIGNATURE, signature); + } + return size; + } + + void readBounceableObject(ByteBuf buf) + { + roomId = Serializer.deserializeLong(buf); + messageId = Serializer.deserializeLong(buf); + senderNickname = (String) Serializer.deserialize(buf, STR_NAME); + signature = (Signature) Serializer.deserialize(buf, SIGNATURE); + } + + public long getRoomId() + { + return roomId; + } + + public void setRoomId(long roomId) + { + this.roomId = roomId; + } + + public long getMessageId() + { + return messageId; + } + + public void setMessageId(long messageId) + { + this.messageId = messageId; + } + + public String getSenderNickname() + { + return senderNickname; + } + + public void setSenderNickname(String senderNickname) + { + this.senderNickname = senderNickname; + } + + public Signature getSignature() + { + return signature; + } + + public void setSignature(Signature signature) + { + this.signature = signature; + } + + @Override + public String toString() + { + return "ChatRoomBounce{" + + "roomId=" + Id.toString(roomId) + + ", messageId=" + Id.toString(messageId) + + ", senderNickname='" + senderNickname + '\'' + + ", signature=[something]" + + '}'; + } +} diff --git a/app/src/main/java/io/xeres/app/xrs/service/chat/item/ChatRoomConfigItem.java b/app/src/main/java/io/xeres/app/xrs/service/chat/item/ChatRoomConfigItem.java new file mode 100644 index 000000000..c93245098 --- /dev/null +++ b/app/src/main/java/io/xeres/app/xrs/service/chat/item/ChatRoomConfigItem.java @@ -0,0 +1,49 @@ +/* + * Copyright (c) 2019-2021 by David Gerber - https://zapek.com + * + * This file is part of Xeres. + * + * Xeres is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Xeres is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Xeres. If not, see . + */ + +package io.xeres.app.xrs.service.chat.item; + +import io.xeres.app.xrs.item.Item; +import io.xeres.app.xrs.item.ItemPriority; +import io.xeres.app.xrs.serialization.RsSerialized; + +public class ChatRoomConfigItem extends Item +{ + @RsSerialized + private long roomId; + + @RsSerialized + private int flags; // XXX: which flags? + + @Override + public int getPriority() + { + return ItemPriority.CHAT.getPriority(); + } + + public long getRoomId() + { + return roomId; + } + + public int getFlags() + { + return flags; + } +} diff --git a/app/src/main/java/io/xeres/app/xrs/service/chat/item/ChatRoomConnectChallengeItem.java b/app/src/main/java/io/xeres/app/xrs/service/chat/item/ChatRoomConnectChallengeItem.java new file mode 100644 index 000000000..3503aca4f --- /dev/null +++ b/app/src/main/java/io/xeres/app/xrs/service/chat/item/ChatRoomConnectChallengeItem.java @@ -0,0 +1,61 @@ +/* + * Copyright (c) 2019-2021 by David Gerber - https://zapek.com + * + * This file is part of Xeres. + * + * Xeres is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Xeres is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Xeres. If not, see . + */ + +package io.xeres.app.xrs.service.chat.item; + +import io.xeres.app.crypto.chatcipher.ChatChallenge; +import io.xeres.app.xrs.item.Item; +import io.xeres.app.xrs.item.ItemPriority; +import io.xeres.app.xrs.serialization.RsSerialized; +import io.xeres.common.id.LocationId; + +public class ChatRoomConnectChallengeItem extends Item +{ + @RsSerialized + private long challengeCode; + + @Override + public int getPriority() + { + return ItemPriority.CHAT.getPriority(); + } + + public ChatRoomConnectChallengeItem() + { + // Required + } + + public ChatRoomConnectChallengeItem(LocationId locationId, long chatRoomId, long messageId) + { + challengeCode = ChatChallenge.code(locationId, chatRoomId, messageId); + } + + public long getChallengeCode() + { + return challengeCode; + } + + @Override + public String toString() + { + return "ChatRoomConnectChallengeItem{" + + "challengeCode=" + challengeCode + + '}'; + } +} diff --git a/app/src/main/java/io/xeres/app/xrs/service/chat/item/ChatRoomEvent.java b/app/src/main/java/io/xeres/app/xrs/service/chat/item/ChatRoomEvent.java new file mode 100644 index 000000000..4e6caba56 --- /dev/null +++ b/app/src/main/java/io/xeres/app/xrs/service/chat/item/ChatRoomEvent.java @@ -0,0 +1,41 @@ +/* + * Copyright (c) 2019-2021 by David Gerber - https://zapek.com + * + * This file is part of Xeres. + * + * Xeres is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Xeres is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Xeres. If not, see . + */ + +package io.xeres.app.xrs.service.chat.item; + +public enum ChatRoomEvent +{ + PEER_LEFT(1), + PEER_STATUS(2), + PEER_JOINED(3), + PEER_CHANGE_NICKNAME(4), + KEEP_ALIVE(5); + + private final int code; + + ChatRoomEvent(int code) + { + this.code = code; + } + + public byte getCode() + { + return (byte) code; + } +} diff --git a/app/src/main/java/io/xeres/app/xrs/service/chat/item/ChatRoomEventItem.java b/app/src/main/java/io/xeres/app/xrs/service/chat/item/ChatRoomEventItem.java new file mode 100644 index 000000000..280e8ae84 --- /dev/null +++ b/app/src/main/java/io/xeres/app/xrs/service/chat/item/ChatRoomEventItem.java @@ -0,0 +1,99 @@ +/* + * Copyright (c) 2019-2021 by David Gerber - https://zapek.com + * + * This file is part of Xeres. + * + * Xeres is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Xeres is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Xeres. If not, see . + */ + +package io.xeres.app.xrs.service.chat.item; + +import io.netty.buffer.ByteBuf; +import io.xeres.app.xrs.serialization.RsSerializable; +import io.xeres.app.xrs.serialization.SerializationFlags; + +import java.time.Instant; +import java.util.Set; + +import static io.xeres.app.xrs.serialization.Serializer.*; +import static io.xeres.app.xrs.serialization.TlvType.STR_NAME; + +public class ChatRoomEventItem extends ChatRoomBounce implements RsSerializable +{ + private byte eventType; + private String status; + private int sendTime; + + public ChatRoomEventItem() + { + // Required + } + + public ChatRoomEventItem(ChatRoomEvent event, String status) + { + this.eventType = event.getCode(); + this.status = status; + this.sendTime = (int) Instant.now().getEpochSecond(); + } + + public byte getEventType() + { + return eventType; + } + + public String getStatus() + { + return status; + } + + public int getSendTime() + { + return sendTime; + } + + @Override + public int writeObject(ByteBuf buf, Set serializationFlags) + { + var size = 0; + + size += serialize(buf, eventType); + size += serialize(buf, STR_NAME, status); + size += serialize(buf, sendTime); + + size += writeBounceableObject(buf, serializationFlags); + + return size; + } + + @Override + public void readObject(ByteBuf buf, Set serializationFlags) + { + eventType = deserializeByte(buf); + status = (String) deserialize(buf, STR_NAME); + sendTime = deserializeInt(buf); + + readBounceableObject(buf); + } + + @Override + public String toString() + { + return "ChatRoomEventItem{" + + "eventType=" + eventType + + ", status='" + status + '\'' + + ", sendTime=" + sendTime + + ", super=" + super.toString() + + '}'; + } +} diff --git a/app/src/main/java/io/xeres/app/xrs/service/chat/item/ChatRoomInviteItem.java b/app/src/main/java/io/xeres/app/xrs/service/chat/item/ChatRoomInviteItem.java new file mode 100644 index 000000000..00e0ffece --- /dev/null +++ b/app/src/main/java/io/xeres/app/xrs/service/chat/item/ChatRoomInviteItem.java @@ -0,0 +1,111 @@ +/* + * Copyright (c) 2019-2021 by David Gerber - https://zapek.com + * + * This file is part of Xeres. + * + * Xeres is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Xeres is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Xeres. If not, see . + */ + +package io.xeres.app.xrs.service.chat.item; + +import io.xeres.app.xrs.item.Item; +import io.xeres.app.xrs.item.ItemPriority; +import io.xeres.app.xrs.serialization.RsSerialized; +import io.xeres.app.xrs.service.chat.RoomFlags; +import io.xeres.common.id.Id; + +import java.util.Set; + +import static io.xeres.app.xrs.serialization.TlvType.STR_NAME; +import static io.xeres.app.xrs.service.chat.RoomFlags.*; + +public class ChatRoomInviteItem extends Item +{ + @RsSerialized + private long roomId; + + @RsSerialized(tlvType = STR_NAME) + private String roomName; + + @RsSerialized(tlvType = STR_NAME) + private String roomTopic; + + @RsSerialized + private Set roomFlags; + + @Override + public int getPriority() + { + return ItemPriority.CHAT.getPriority(); + } + + public ChatRoomInviteItem() + { + // Needed + } + + public ChatRoomInviteItem(long roomId, String roomName, String roomTopic, Set roomFlags) + { + this.roomId = roomId; + this.roomName = roomName; + this.roomTopic = roomTopic; + this.roomFlags = roomFlags; + } + + public long getRoomId() + { + return roomId; + } + + public String getRoomName() + { + return roomName; + } + + public String getRoomTopic() + { + return roomTopic; + } + + public Set getRoomFlags() + { + return roomFlags; + } + + public boolean isConnectionChallenge() + { + return roomFlags.contains(CHALLENGE); + } + + public boolean isPublic() + { + return roomFlags.contains(PUBLIC); + } + + public boolean isSigned() + { + return roomFlags.contains(PGP_SIGNED); + } + + @Override + public String toString() + { + return "ChatRoomInviteItem{" + + "roomId=" + Id.toString(roomId) + + ", roomName='" + roomName + '\'' + + ", roomTopic='" + roomTopic + '\'' + + ", roomFlags=" + roomFlags + + '}'; + } +} diff --git a/app/src/main/java/io/xeres/app/xrs/service/chat/item/ChatRoomListItem.java b/app/src/main/java/io/xeres/app/xrs/service/chat/item/ChatRoomListItem.java new file mode 100644 index 000000000..6512d679a --- /dev/null +++ b/app/src/main/java/io/xeres/app/xrs/service/chat/item/ChatRoomListItem.java @@ -0,0 +1,62 @@ +/* + * Copyright (c) 2019-2021 by David Gerber - https://zapek.com + * + * This file is part of Xeres. + * + * Xeres is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Xeres is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Xeres. If not, see . + */ + +package io.xeres.app.xrs.service.chat.item; + +import io.xeres.app.xrs.item.Item; +import io.xeres.app.xrs.item.ItemPriority; +import io.xeres.app.xrs.serialization.RsSerialized; + +import java.util.ArrayList; +import java.util.List; + +public class ChatRoomListItem extends Item +{ + @RsSerialized + private final List chatRooms = new ArrayList<>(); + + public ChatRoomListItem() + { + // Required + } + + public ChatRoomListItem(List chatRooms) + { + this.chatRooms.addAll(chatRooms); + } + + @Override + public int getPriority() + { + return ItemPriority.CHAT.getPriority(); + } + + @Override + public String toString() + { + return "ChatRoomListItem{" + + "chatRooms=" + chatRooms + + '}'; + } + + public List getChatRooms() + { + return chatRooms; + } +} diff --git a/app/src/main/java/io/xeres/app/xrs/service/chat/item/ChatRoomListRequestItem.java b/app/src/main/java/io/xeres/app/xrs/service/chat/item/ChatRoomListRequestItem.java new file mode 100644 index 000000000..0877b845c --- /dev/null +++ b/app/src/main/java/io/xeres/app/xrs/service/chat/item/ChatRoomListRequestItem.java @@ -0,0 +1,40 @@ +/* + * Copyright (c) 2019-2021 by David Gerber - https://zapek.com + * + * This file is part of Xeres. + * + * Xeres is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Xeres is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Xeres. If not, see . + */ + +package io.xeres.app.xrs.service.chat.item; + +import io.xeres.app.xrs.item.Item; +import io.xeres.app.xrs.item.ItemPriority; + +public class ChatRoomListRequestItem extends Item +{ + // This is an empty item + + @Override + public int getPriority() + { + return ItemPriority.CHAT.getPriority(); + } + + @Override + public String toString() + { + return "ChatRoomListRequestItem{}"; + } +} diff --git a/app/src/main/java/io/xeres/app/xrs/service/chat/item/ChatRoomMessageItem.java b/app/src/main/java/io/xeres/app/xrs/service/chat/item/ChatRoomMessageItem.java new file mode 100644 index 000000000..17561152d --- /dev/null +++ b/app/src/main/java/io/xeres/app/xrs/service/chat/item/ChatRoomMessageItem.java @@ -0,0 +1,102 @@ +/* + * Copyright (c) 2019-2021 by David Gerber - https://zapek.com + * + * This file is part of Xeres. + * + * Xeres is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Xeres is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Xeres. If not, see . + */ + +package io.xeres.app.xrs.service.chat.item; + +import io.netty.buffer.ByteBuf; +import io.xeres.app.xrs.serialization.FieldSize; +import io.xeres.app.xrs.serialization.RsSerializable; +import io.xeres.app.xrs.serialization.SerializationFlags; +import io.xeres.app.xrs.service.chat.ChatFlags; + +import java.time.Instant; +import java.util.EnumSet; +import java.util.Set; + +import static io.xeres.app.xrs.serialization.Serializer.*; +import static io.xeres.app.xrs.serialization.TlvType.STR_MSG; +import static io.xeres.app.xrs.service.chat.ChatFlags.LOBBY; +import static io.xeres.app.xrs.service.chat.ChatFlags.PRIVATE; + +public class ChatRoomMessageItem extends ChatRoomBounce implements RsSerializable +{ + private Set flags; + private int sendTime; + private String message; + private long parentMessageId; + + public ChatRoomMessageItem() + { + // Needed + } + + public ChatRoomMessageItem(String message) + { + this.flags = EnumSet.of(LOBBY, PRIVATE); + this.sendTime = (int) Instant.now().getEpochSecond(); + this.message = message; + this.parentMessageId = 0L; + } + + public Set getFlags() + { + return flags; + } + + public int getSendTime() + { + return sendTime; + } + + public String getMessage() + { + return message; + } + + public long getParentMessageId() + { + return parentMessageId; + } + + @Override + public int writeObject(ByteBuf buf, Set serializationFlags) + { + var size = 0; + + size += serialize(buf, flags, FieldSize.INTEGER); + size += serialize(buf, sendTime); + size += serialize(buf, STR_MSG, message); + size += serialize(buf, parentMessageId); + + size += writeBounceableObject(buf, serializationFlags); + + return size; + } + + @Override + public void readObject(ByteBuf buf, Set serializationFlags) + { + flags = deserializeEnumSet(buf, ChatFlags.class, FieldSize.INTEGER); + sendTime = deserializeInt(buf); + message = (String) deserialize(buf, STR_MSG); + parentMessageId = deserializeLong(buf); + + readBounceableObject(buf); + } +} diff --git a/app/src/main/java/io/xeres/app/xrs/service/chat/item/ChatRoomUnsubscribeItem.java b/app/src/main/java/io/xeres/app/xrs/service/chat/item/ChatRoomUnsubscribeItem.java new file mode 100644 index 000000000..fa33aad4f --- /dev/null +++ b/app/src/main/java/io/xeres/app/xrs/service/chat/item/ChatRoomUnsubscribeItem.java @@ -0,0 +1,51 @@ +/* + * Copyright (c) 2019-2021 by David Gerber - https://zapek.com + * + * This file is part of Xeres. + * + * Xeres is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Xeres is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Xeres. If not, see . + */ + +package io.xeres.app.xrs.service.chat.item; + +import io.xeres.app.xrs.item.Item; +import io.xeres.app.xrs.item.ItemPriority; +import io.xeres.app.xrs.serialization.RsSerialized; + +public class ChatRoomUnsubscribeItem extends Item +{ + @RsSerialized + private long roomId; + + @Override + public int getPriority() + { + return ItemPriority.CHAT.getPriority(); + } + + public ChatRoomUnsubscribeItem() + { + // Needed + } + + public ChatRoomUnsubscribeItem(long roomId) + { + this.roomId = roomId; + } + + public long getRoomId() + { + return roomId; + } +} diff --git a/app/src/main/java/io/xeres/app/xrs/service/chat/item/ChatStatusItem.java b/app/src/main/java/io/xeres/app/xrs/service/chat/item/ChatStatusItem.java new file mode 100644 index 000000000..2e45fa8f7 --- /dev/null +++ b/app/src/main/java/io/xeres/app/xrs/service/chat/item/ChatStatusItem.java @@ -0,0 +1,74 @@ +/* + * Copyright (c) 2019-2021 by David Gerber - https://zapek.com + * + * This file is part of Xeres. + * + * Xeres is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Xeres is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Xeres. If not, see . + */ + +package io.xeres.app.xrs.service.chat.item; + +import io.xeres.app.xrs.item.Item; +import io.xeres.app.xrs.item.ItemPriority; +import io.xeres.app.xrs.serialization.RsSerialized; +import io.xeres.app.xrs.service.chat.ChatFlags; + +import java.util.Set; + +import static io.xeres.app.xrs.serialization.TlvType.STR_MSG; + +public class ChatStatusItem extends Item +{ + @RsSerialized + private Set flags; + + @RsSerialized(tlvType = STR_MSG) + private String status; + + public ChatStatusItem() + { + // Required + } + + public ChatStatusItem(String status, Set flags) + { + this.status = status; + this.flags = flags; + } + + @Override + public int getPriority() + { + return ItemPriority.CHAT.getPriority(); + } + + public Set getFlags() + { + return flags; + } + + public String getStatus() + { + return status; + } + + @Override + public String toString() + { + return "ChatStatusItem{" + + "flags=" + flags + + ", status='" + status + '\'' + + '}'; + } +} diff --git a/app/src/main/java/io/xeres/app/xrs/service/chat/item/PrivateChatMessageConfigItem.java b/app/src/main/java/io/xeres/app/xrs/service/chat/item/PrivateChatMessageConfigItem.java new file mode 100644 index 000000000..d5334ff40 --- /dev/null +++ b/app/src/main/java/io/xeres/app/xrs/service/chat/item/PrivateChatMessageConfigItem.java @@ -0,0 +1,84 @@ +/* + * Copyright (c) 2019-2021 by David Gerber - https://zapek.com + * + * This file is part of Xeres. + * + * Xeres is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Xeres is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Xeres. If not, see . + */ + +package io.xeres.app.xrs.service.chat.item; + +import io.xeres.app.xrs.item.Item; +import io.xeres.app.xrs.item.ItemPriority; +import io.xeres.app.xrs.serialization.RsSerialized; +import io.xeres.common.id.LocationId; + +import static io.xeres.app.xrs.serialization.TlvType.STR_MSG; + +public class PrivateChatMessageConfigItem extends Item +{ + @RsSerialized + private LocationId locationId; + + @RsSerialized + private int chatFlags; // XXX: enumsets + + @RsSerialized + private int configFlags; // XXX: use an enumSet + + @RsSerialized + private int sendTime; + + @RsSerialized(tlvType = STR_MSG) + private String message; + + @RsSerialized + int receiveTime; + + @Override + public int getPriority() + { + return ItemPriority.CHAT.getPriority(); + } + + public LocationId getLocationId() + { + return locationId; + } + + public int getChatFlags() + { + return chatFlags; + } + + public int getConfigFlags() + { + return configFlags; + } + + public int getSendTime() + { + return sendTime; + } + + public String getMessage() + { + return message; + } + + public int getReceiveTime() + { + return receiveTime; + } +} diff --git a/app/src/main/java/io/xeres/app/xrs/service/chat/item/PrivateOutgoingMapItem.java b/app/src/main/java/io/xeres/app/xrs/service/chat/item/PrivateOutgoingMapItem.java new file mode 100644 index 000000000..51ab97671 --- /dev/null +++ b/app/src/main/java/io/xeres/app/xrs/service/chat/item/PrivateOutgoingMapItem.java @@ -0,0 +1,43 @@ +/* + * Copyright (c) 2019-2021 by David Gerber - https://zapek.com + * + * This file is part of Xeres. + * + * Xeres is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Xeres is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Xeres. If not, see . + */ + +package io.xeres.app.xrs.service.chat.item; + +import io.xeres.app.xrs.item.Item; +import io.xeres.app.xrs.item.ItemPriority; +import io.xeres.app.xrs.serialization.RsSerialized; + +import java.util.Map; + +public class PrivateOutgoingMapItem extends Item +{ + @RsSerialized + private Map store; + + @Override + public int getPriority() + { + return ItemPriority.CHAT.getPriority(); + } + + public Map getStore() + { + return store; + } +} diff --git a/app/src/main/java/io/xeres/app/xrs/service/chat/item/SubscribedChatRoomConfigItem.java b/app/src/main/java/io/xeres/app/xrs/service/chat/item/SubscribedChatRoomConfigItem.java new file mode 100644 index 000000000..f56177edf --- /dev/null +++ b/app/src/main/java/io/xeres/app/xrs/service/chat/item/SubscribedChatRoomConfigItem.java @@ -0,0 +1,96 @@ +/* + * Copyright (c) 2019-2021 by David Gerber - https://zapek.com + * + * This file is part of Xeres. + * + * Xeres is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Xeres is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Xeres. If not, see . + */ + +package io.xeres.app.xrs.service.chat.item; + +import io.xeres.app.xrs.item.Item; +import io.xeres.app.xrs.serialization.RsSerialized; +import io.xeres.app.xrs.service.chat.RoomFlags; +import io.xeres.common.id.GxsId; +import io.xeres.common.id.LocationId; + +import java.util.Map; +import java.util.Set; + +public class SubscribedChatRoomConfigItem extends Item +{ + @RsSerialized + private long roomId; + + @RsSerialized + private String roomName; + + @RsSerialized + private String roomTopic; + + @RsSerialized + private Set participatingLocations; // XXX: do we serialize Sets yet? + + @RsSerialized + private GxsId gxsId; + + @RsSerialized + private Set flags; + + @RsSerialized + private Map gxsIds; + + @RsSerialized + private long lastActivity; + + public long getRoomId() + { + return roomId; + } + + public String getRoomName() + { + return roomName; + } + + public String getRoomTopic() + { + return roomTopic; + } + + public Set getParticipatingLocations() + { + return participatingLocations; + } + + public GxsId getGxsId() + { + return gxsId; + } + + public Set getFlags() + { + return flags; + } + + public Map getGxsIds() + { + return gxsIds; + } + + public long getLastActivity() + { + return lastActivity; + } +} diff --git a/app/src/main/java/io/xeres/app/xrs/service/chat/item/VisibleChatRoomInfo.java b/app/src/main/java/io/xeres/app/xrs/service/chat/item/VisibleChatRoomInfo.java new file mode 100644 index 000000000..41579fef7 --- /dev/null +++ b/app/src/main/java/io/xeres/app/xrs/service/chat/item/VisibleChatRoomInfo.java @@ -0,0 +1,96 @@ +/* + * Copyright (c) 2019-2021 by David Gerber - https://zapek.com + * + * This file is part of Xeres. + * + * Xeres is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Xeres is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Xeres. If not, see . + */ + +package io.xeres.app.xrs.service.chat.item; + +import io.xeres.app.xrs.serialization.RsSerialized; +import io.xeres.app.xrs.service.chat.RoomFlags; + +import java.util.Set; + +import static io.xeres.app.xrs.serialization.TlvType.STR_NAME; + +public class VisibleChatRoomInfo +{ + @RsSerialized + private long id; + + @RsSerialized(tlvType = STR_NAME) + private String name; + + @RsSerialized(tlvType = STR_NAME) + private String topic; + + @RsSerialized + private int count; + + @RsSerialized + private Set flags; + + public VisibleChatRoomInfo() + { + // Required + } + + public VisibleChatRoomInfo(long id, String name, String topic, int count, Set roomFlags) + { + this.id = id; + this.name = name; + this.topic = topic; + this.count = count; + this.flags = roomFlags; + } + + public long getId() + { + return id; + } + + public String getName() + { + return name; + } + + public String getTopic() + { + return topic; + } + + public int getCount() + { + return count; + } + + public Set getFlags() + { + return flags; + } + + @Override + public String toString() + { + return "VisibleChatRoomInfo{" + + "id=" + id + + ", name='" + name + '\'' + + ", topic='" + topic + '\'' + + ", count=" + count + + ", flags=" + flags + + '}'; + } +} diff --git a/app/src/main/java/io/xeres/app/xrs/service/discovery/DiscoveryService.java b/app/src/main/java/io/xeres/app/xrs/service/discovery/DiscoveryService.java new file mode 100644 index 000000000..863287a15 --- /dev/null +++ b/app/src/main/java/io/xeres/app/xrs/service/discovery/DiscoveryService.java @@ -0,0 +1,467 @@ +/* + * Copyright (c) 2019-2021 by David Gerber - https://zapek.com + * + * This file is part of Xeres. + * + * Xeres is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Xeres is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Xeres. If not, see . + */ + +package io.xeres.app.xrs.service.discovery; + +import io.xeres.app.crypto.pgp.PGP; +import io.xeres.app.database.DatabaseSession; +import io.xeres.app.database.DatabaseSessionManager; +import io.xeres.app.database.model.connection.Connection; +import io.xeres.app.database.model.identity.Identity; +import io.xeres.app.database.model.location.Location; +import io.xeres.app.database.model.profile.Profile; +import io.xeres.app.net.peer.PeerConnection; +import io.xeres.app.net.peer.PeerConnectionManager; +import io.xeres.app.net.protocol.PeerAddress; +import io.xeres.app.service.IdentityService; +import io.xeres.app.service.LocationService; +import io.xeres.app.service.ProfileService; +import io.xeres.app.xrs.item.Item; +import io.xeres.app.xrs.service.RsService; +import io.xeres.app.xrs.service.RsServiceInitPriority; +import io.xeres.app.xrs.service.RsServiceType; +import io.xeres.app.xrs.service.discovery.item.DiscoveryContactItem; +import io.xeres.app.xrs.service.discovery.item.DiscoveryIdentityListItem; +import io.xeres.app.xrs.service.discovery.item.DiscoveryPgpKeyItem; +import io.xeres.app.xrs.service.discovery.item.DiscoveryPgpListItem; +import io.xeres.app.xrs.service.gxsid.GxsIdService; +import io.xeres.common.id.Id; +import io.xeres.common.id.ProfileFingerprint; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.boot.info.BuildProperties; +import org.springframework.core.env.Environment; +import org.springframework.stereotype.Component; +import org.springframework.transaction.annotation.Transactional; + +import java.io.IOException; +import java.net.SocketAddress; +import java.security.InvalidKeyException; +import java.time.Instant; +import java.util.*; +import java.util.concurrent.TimeUnit; + +import static io.xeres.app.xrs.service.RsServiceType.GOSSIP_DISCOVERY; +import static java.util.function.Predicate.not; +import static java.util.stream.Collectors.toSet; + +@Component +public class DiscoveryService extends RsService +{ + private static final Logger log = LoggerFactory.getLogger(DiscoveryService.class); + + private final ProfileService profileService; + private final LocationService locationService; + private final IdentityService identityService; + private final GxsIdService gxsIdService; + private final BuildProperties buildProperties; + private final DatabaseSessionManager databaseSessionManager; + + public DiscoveryService(Environment environment, PeerConnectionManager peerConnectionManager, ProfileService profileService, LocationService locationService, IdentityService identityService, GxsIdService gxsIdService, BuildProperties buildProperties, DatabaseSessionManager databaseSessionManager) + { + super(environment, peerConnectionManager); + this.profileService = profileService; + this.locationService = locationService; + this.identityService = identityService; + this.gxsIdService = gxsIdService; + this.buildProperties = buildProperties; + this.databaseSessionManager = databaseSessionManager; + } + + @Override + public RsServiceType getServiceType() + { + return GOSSIP_DISCOVERY; + } + + @Override + public Map, Integer> getSupportedItems() + { + return Map.of( + DiscoveryPgpListItem.class, 1, + DiscoveryContactItem.class, 5, + DiscoveryIdentityListItem.class, 6, + DiscoveryPgpKeyItem.class, 9 + ); + } + + @Override + public RsServiceInitPriority getInitPriority() + { + return RsServiceInitPriority.NORMAL; + } + + @Override + public void initialize(PeerConnection peerConnection) + { + peerConnection.schedule( + () -> sendOwnContactAndIdentities(peerConnection) + , 0, + TimeUnit.SECONDS + ); + } + + private void sendOwnContactAndIdentities(PeerConnection peerConnection) + { + try (var session = new DatabaseSession(databaseSessionManager)) + { + var ownLocation = locationService.findOwnLocation().orElseThrow(); + sendContact(peerConnection, ownLocation); + sendIdentity(peerConnection, identityService.getOwnIdentity()); // XXX: in the future we will have several identities, just get the signed ones here + // XXX: also send our own other locations, if any (ie. laptop, etc...). XXX: this should be already done in the current code but check. it is done when the peer sends us his list of friends and has us in it + } + } + + private void sendContact(Location toLocation, Location aboutLocation) + { + sendContact(toLocation, aboutLocation, null); + } + + private void sendContact(PeerConnection peerConnection, Location aboutLocation) + { + sendContact(peerConnection.getLocation(), aboutLocation, peerConnection.getCtx().channel().remoteAddress()); + } + + private void sendContact(Location toLocation, Location aboutLocation, SocketAddress toLocationAddress) + { + log.debug("Sending contact information of {} to {}", aboutLocation, toLocation); + + var builder = DiscoveryContactItem.builder(); + + builder.setPgpIdentifier(aboutLocation.getProfile().getPgpIdentifier()); + builder.setLocationId(aboutLocation.getLocationId()); + builder.setLocationName(aboutLocation.getName()); + if (aboutLocation.isOwn()) + { + builder.setVersion(buildProperties.getName() + " " + buildProperties.getVersion()); + } + builder.setNetMode(aboutLocation.getNetMode()); + builder.setVsDisc(aboutLocation.isDiscoverable() ? 2 : 0); + builder.setVsDht(aboutLocation.isDht() ? 2 : 0); + builder.setLastContact((int) (aboutLocation.getLastConnected() != null ? aboutLocation.getLastConnected().getEpochSecond() : Instant.now().getEpochSecond())); // RS uses Instant.now() XXX: find out if there is any issue with that change. it tells since how long we've been connected + aboutLocation.getConnections().stream() + .filter(not(Connection::isExternal)) + .findFirst() + .ifPresent(connection -> builder.setLocalAddressV4(PeerAddress.fromAddress(connection.getAddress()))); + aboutLocation.getConnections().stream() + .filter(Connection::isExternal) + .findFirst() + .ifPresent(connection -> builder.setExternalAddressV4(PeerAddress.fromAddress(connection.getAddress()))); + if (aboutLocation.equals(toLocation) && toLocationAddress != null) + { + // Tell the peer about how we see its IP address + builder.setCurrentConnectAddress(PeerAddress.fromSocketAddress(toLocationAddress)); + } + // XXX: missing hostname support + writeItem(toLocation, builder.build()); + } + + private void sendIdentity(PeerConnection peerConnection, Identity identity) + { + log.debug("Sending our own identity {} to {}", identity, peerConnection); + + writeItem(peerConnection, new DiscoveryIdentityListItem(List.of(identity.getGxsIdGroupItem().getGxsId()))); + } + + private void askForPgpKeys(PeerConnection peerConnection, Set pgpIds) + { + var pgpListItem = new DiscoveryPgpListItem(DiscoveryPgpListItem.Mode.GET_CERT, pgpIds); + writeItem(peerConnection, pgpListItem); + } + + private void sendOwnContacts(PeerConnection peerConnection) + { + if (!locationService.findOwnLocation().orElseThrow().isDiscoverable()) + { + return; + } + + Set pgpIds = profileService.getAllDiscoverableProfiles().stream() + .map(Profile::getPgpIdentifier) + .collect(toSet()); + + log.debug("Sending list of friends..."); + assert !pgpIds.isEmpty(); + writeItem(peerConnection, new DiscoveryPgpListItem(DiscoveryPgpListItem.Mode.FRIENDS, pgpIds)); + } + + @Transactional + @Override + public void handleItem(PeerConnection peerConnection, Item item) + { + if (item instanceof DiscoveryContactItem discoveryContactItem) + { + handleContact(peerConnection, discoveryContactItem); + } + else if (item instanceof DiscoveryIdentityListItem discoveryIdentityListItem) + { + handleIdentityList(peerConnection, discoveryIdentityListItem); + } + else if (item instanceof DiscoveryPgpListItem discoveryPgpListItem) + { + handlePgpList(peerConnection, discoveryPgpListItem); + } + else if (item instanceof DiscoveryPgpKeyItem discoveryPgpKeyItem) + { + handlePgpKey(peerConnection, discoveryPgpKeyItem); + } + } + + private void handleContact(PeerConnection peerConnection, DiscoveryContactItem discoveryContactItem) + { + var peerLocation = peerConnection.getLocation(); + Optional contactLocation = locationService.findLocationById(discoveryContactItem.getLocationId()); + + if (contactLocation.isPresent()) + { + if (contactLocation.get().equals(peerLocation)) + { + // Contact information of the peer + updateConnectedContact(peerConnection, discoveryContactItem, peerLocation, contactLocation.get()); + } + else if (contactLocation.get().equals(locationService.findOwnLocation().orElseThrow())) + { + // Contact information about ourself (this can be used to help us find our external IP address + updateOwnContactLocation(discoveryContactItem); + } + else + { + // Contact information about our friends + updateCommonContactLocation(peerConnection, discoveryContactItem, contactLocation.get()); + } + } + else + { + // New locations + addNewContactLocation(discoveryContactItem); + } + } + + private void updateConnectedContact(PeerConnection peerConnection, DiscoveryContactItem discoveryContactItem, Location peerLocation, Location contactLocation) + { + log.debug("LocationId of peer itself"); + if (discoveryContactItem.getPgpIdentifier() != contactLocation.getProfile().getPgpIdentifier()) + { + log.error("PGP identifier or peer doesn't match the key we have about him. Ignoring."); + return; + } + + var updatedLocation = updateLocation(peerLocation, discoveryContactItem); + peerConnection.updateLocation(updatedLocation); + + if (peerLocation.getProfile().isPartial()) + { + // Ask for its PGP public key + log.debug("Asking for PGP public key of peer"); + askForPgpKeys(peerConnection, Set.of(peerLocation.getProfile().getPgpIdentifier())); + } + else + { + // Send our friends + sendOwnContacts(peerConnection); + } + } + + private void updateOwnContactLocation(DiscoveryContactItem discoveryContactItem) + { + log.debug("Peer is sending our own location, IP: {}", discoveryContactItem.getCurrentConnectAddress()); + // XXX: process the IP in case we don't find our external address and it could help + // XXX: beware! RS seems to send ipv4 address in the ipv6 structure... + // XXX: comments also seem to suggest this can be used to check if the connected IP is the same as our external IP + } + + private void updateCommonContactLocation(PeerConnection peerConnection, DiscoveryContactItem discoveryContactItem, Location contactLocation) + { + if (contactLocation.getProfile().isAccepted()) + { + log.debug("Would update friend here"); + var updatedLocation = updateLocation(contactLocation, discoveryContactItem); + peerConnection.updateLocation(updatedLocation); + } + } + + private void addNewContactLocation(DiscoveryContactItem discoveryContactItem) + { + log.debug("New location"); + + Optional profile = profileService.findProfileByPgpIdentifier(discoveryContactItem.getPgpIdentifier()); + if (profile.isPresent()) + { + if (profile.get().isAccepted()) + { + // New location of a friend + var newLocation = Location.createLocation(discoveryContactItem.getLocationName(), profile.get(), discoveryContactItem.getLocationId()); + newLocation = updateLocation(newLocation, discoveryContactItem); + log.debug("New location of a friend, added: {}", newLocation); + } + else + { + // Friend of friend, but shouldn't happen because RS only sends common contacts. + log.debug("New location for profile {} that we have but is not a friend, ignoring...", profile.get()); + } + } + else + { + // Friend of friend, but shouldn't happen because RS only sends common contacts + // We don't have any use for those. RS uses them as potential proxies/relays for the DHT but I have + // yet to see this in the wild because it shouldn't happen. + log.debug("New location for friend of friend {}, ignoring...", log.isDebugEnabled() ? Id.toString(discoveryContactItem.getPgpIdentifier()) : ""); + } + } + + private Location updateLocation(Location location, DiscoveryContactItem discoveryContactItem) + { + var addresses = new ArrayList(); + if (discoveryContactItem.getExternalAddressV4() != null) + { + addresses.add(discoveryContactItem.getExternalAddressV4()); + } + if (discoveryContactItem.getLocalAddressV4() != null) + { + addresses.add(discoveryContactItem.getLocalAddressV4()); + } + addresses.addAll(discoveryContactItem.getExternalAddressList()); + + return locationService.update( + location, + discoveryContactItem.getLocationName(), + discoveryContactItem.getNetMode(), + discoveryContactItem.getVersion(), + discoveryContactItem.getVsDisc() == 2, + discoveryContactItem.getVsDht() == 2, + addresses, + discoveryContactItem.getHostname()); + } + + private void handlePgpList(PeerConnection peerConnection, DiscoveryPgpListItem discoveryPgpListItem) + { + var ownLocation = locationService.findOwnLocation().orElseThrow(); + if (!ownLocation.isDiscoverable()) + { + return; + } + + if (discoveryPgpListItem.getMode() == DiscoveryPgpListItem.Mode.GET_CERT) + { + List friends = getMutualFriends(discoveryPgpListItem.getPgpIds()); + + friends.forEach(profile -> writeItem(peerConnection, new DiscoveryPgpKeyItem(profile.getPgpIdentifier(), profile.getPgpPublicKeyData()))); // XXX: RS does that slowly it seems... about one key every few seconds + } + else if (discoveryPgpListItem.getMode() == DiscoveryPgpListItem.Mode.FRIENDS) + { + // The peer sent us his list of friends. + log.debug("Received peer's list of friends"); + + // Only ask for the ones we don't already have. + Set unknownFriendIds = discoveryPgpListItem.getPgpIds().stream() + .filter(pgpId -> profileService.findProfileByPgpIdentifier(pgpId).isEmpty()) + .collect(toSet()); + + if (!unknownFriendIds.isEmpty()) + { + askForPgpKeys(peerConnection, unknownFriendIds); + } + + // Send contact info of all mutual friends with discovery enabled to peer, + // including the peer itself if it wants to and also our other locations. + List mutualFriends = getMutualFriends(discoveryPgpListItem.getPgpIds()); + List locationsToSend = mutualFriends.stream() + .map(Profile::getLocations) + .flatMap(List::stream) + .filter(location -> !location.equals(ownLocation)) // own location was sent at beginning + .toList(); + + locationsToSend.forEach(location -> sendContact(peerConnection, location)); + + // Inform all our online mutual friends about peer (except itself as we just sent it above). + locationsToSend.stream() + .filter(location -> !location.equals(peerConnection.getLocation()) && location.isConnected()) + .forEach(location -> sendContact(location, peerConnection.getLocation())); + } + } + + private List getMutualFriends(Set pgpIds) + { + return pgpIds.stream() + .map(profileService::findDiscoverableProfileByPgpIdentifier) + .flatMap(Optional::stream) + .toList(); + } + + private void handlePgpKey(PeerConnection peerConnection, DiscoveryPgpKeyItem discoveryPgpKeyItem) + { + try + { + log.debug("Got PGP key for ID {}", log.isDebugEnabled() ? Id.toString(discoveryPgpKeyItem.getPgpIdentifier()) : ""); + + var pgpPublicKey = PGP.getPGPPublicKey(discoveryPgpKeyItem.getKeyData()); + + if (discoveryPgpKeyItem.getPgpIdentifier() != pgpPublicKey.getKeyID()) + { + log.warn("PGP key from {} has an ID ({}) which doesn't match the advertised ID {}", peerConnection.getLocation(), pgpPublicKey.getKeyID(), discoveryPgpKeyItem.getPgpIdentifier()); + return; + } + + var profileFingerprint = new ProfileFingerprint(pgpPublicKey.getFingerprint()); + Optional profile = profileService.findProfileByPgpFingerprint(profileFingerprint); + if (profile.isPresent()) + { + if (profile.get().isPartial()) + { + // The PGP key is about a partial profile, thoroughly check if the peer is the partial profile itself + if (discoveryPgpKeyItem.getPgpIdentifier() == peerConnection.getLocation().getProfile().getPgpIdentifier() // Incoming key PGP id is the one of the remote peer + && profile.get().getPgpIdentifier() == peerConnection.getLocation().getProfile().getPgpIdentifier() // ShortInvite PGP ID matches remote peer + && profileFingerprint.equals(peerConnection.getLocation().getProfile().getProfileFingerprint())) // Incoming key fingerprint matches remote peer + { + // We can save its PGP key and promote it to full profile. + profile.get().setPgpPublicKeyData(discoveryPgpKeyItem.getKeyData()); + profileService.createOrUpdateProfile(profile.get()); + + sendOwnContacts(peerConnection); + } + } + else + { + // XXX: check the key and complain if it doesn't match + } + } + else + { + // Create a new profile and save the key + log.debug("Creating new profile for id {}", log.isDebugEnabled() ? Id.toString(discoveryPgpKeyItem.getPgpIdentifier()) : ""); + var newProfile = Profile.createProfile(pgpPublicKey.getUserIDs().next(), pgpPublicKey.getKeyID(), new ProfileFingerprint(pgpPublicKey.getFingerprint()), pgpPublicKey.getEncoded()); + profileService.createOrUpdateProfile(newProfile); + } + } + catch (InvalidKeyException e) + { + log.warn("Invalid PGP public key for profile id {}", Id.toString(discoveryPgpKeyItem.getPgpIdentifier())); + } + catch (IOException e) + { + log.error("Error while reading PGP public key: {}", e.getMessage()); + } + } + + private void handleIdentityList(PeerConnection peerConnection, DiscoveryIdentityListItem discoveryIdentityListItem) + { + log.debug("Got identities from friend: {}, requesting...", discoveryIdentityListItem); + gxsIdService.requestGxsGroups(peerConnection, discoveryIdentityListItem.getIdentities()); + } +} diff --git a/app/src/main/java/io/xeres/app/xrs/service/discovery/item/DiscoveryContactItem.java b/app/src/main/java/io/xeres/app/xrs/service/discovery/item/DiscoveryContactItem.java new file mode 100644 index 000000000..50a46ddd8 --- /dev/null +++ b/app/src/main/java/io/xeres/app/xrs/service/discovery/item/DiscoveryContactItem.java @@ -0,0 +1,421 @@ +/* + * Copyright (c) 2019-2021 by David Gerber - https://zapek.com + * + * This file is part of Xeres. + * + * Xeres is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Xeres is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Xeres. If not, see . + */ + +package io.xeres.app.xrs.service.discovery.item; + +import io.netty.buffer.ByteBuf; +import io.xeres.app.net.protocol.PeerAddress; +import io.xeres.app.xrs.item.Item; +import io.xeres.app.xrs.serialization.RsSerializable; +import io.xeres.app.xrs.serialization.SerializationFlags; +import io.xeres.common.id.Id; +import io.xeres.common.id.LocationId; +import io.xeres.common.protocol.NetMode; + +import java.util.ArrayList; +import java.util.List; +import java.util.Set; + +import static io.xeres.app.xrs.serialization.Serializer.*; +import static io.xeres.app.xrs.serialization.TlvType.*; + +public class DiscoveryContactItem extends Item implements RsSerializable +{ + private long pgpIdentifier; + private LocationId locationId; + private String locationName; + private String version; + private NetMode netMode; // 1: UDP, 2: UPNP, 3: EXT, 4: HIDDEN, 5: UNREACHABLE + private short vsDisc; // 0: off, 1: minimal (never implemented I think), 2: full + private short vsDht; // 0: off, 1: passive (never implemented too?!), 2: full + private int lastContact; + + private String hiddenAddress; + private short hiddenPort; + + private PeerAddress localAddressV4; + private PeerAddress externalAddressV4; + private PeerAddress localAddressV6; + private PeerAddress externalAddressV6; + private PeerAddress currentConnectAddress; + private String hostname; + private List localAddressList = new ArrayList<>(); + private List externalAddressList = new ArrayList<>(); + + @SuppressWarnings("unused") + public DiscoveryContactItem() + { + } + + private DiscoveryContactItem(Builder builder) + { + pgpIdentifier = builder.pgpIdentifier; + locationId = builder.locationId; + locationName = builder.location; + version = builder.version; + netMode = builder.netMode; + vsDisc = builder.vsDisc; + vsDht = builder.vsDht; + lastContact = builder.lastContact; + hiddenAddress = builder.hiddenAddress; + hiddenPort = builder.hiddenPort; + localAddressV4 = builder.localAddressV4; + externalAddressV4 = builder.externalAddressV4; + localAddressV6 = builder.localAddressV6; + externalAddressV6 = builder.externalAddressV6; + currentConnectAddress = builder.currentConnectAddress; + hostname = builder.hostname; + if (builder.localAddressList != null) + { + localAddressList = builder.localAddressList; + } + if (builder.externalAddressList != null) + { + externalAddressList = builder.externalAddressList; + } + } + + public static Builder builder() + { + return new Builder(); + } + + @Override + public int writeObject(ByteBuf buf, Set serializationFlags) + { + var size = 0; + + size += serialize(buf, pgpIdentifier); + size += serialize(buf, locationId); + size += serialize(buf, STR_LOCATION, locationName); + size += serialize(buf, STR_VERSION, version); + size += serialize(buf, netMode); + size += serialize(buf, vsDisc); + size += serialize(buf, vsDht); + size += serialize(buf, lastContact); + + if (hiddenAddress != null) + { + size += serialize(buf, STR_DOM_ADDR, hiddenAddress); + size += serialize(buf, hiddenPort); + } + else + { + size += serialize(buf, ADDRESS, localAddressV4); + size += serialize(buf, ADDRESS, externalAddressV4); + size += serialize(buf, ADDRESS, localAddressV6); + size += serialize(buf, ADDRESS, externalAddressV6); + size += serialize(buf, ADDRESS, currentConnectAddress); + size += serialize(buf, STR_DYNDNS, hostname); + + size += serialize(buf, ADDRESS_SET, localAddressList); + size += serialize(buf, ADDRESS_SET, externalAddressList); + } + return size; + } + + @Override + @SuppressWarnings("unchecked") + public void readObject(ByteBuf buf, Set serializationFlags) + { + pgpIdentifier = deserializeLong(buf); + locationId = (LocationId) deserializeIdentifier(buf, LocationId.class); + locationName = (String) deserialize(buf, STR_LOCATION); + version = (String) deserialize(buf, STR_VERSION); + netMode = deserializeEnum(buf, NetMode.class); + vsDisc = deserializeShort(buf); + vsDht = deserializeShort(buf); + lastContact = deserializeInt(buf); + + if (buf.getUnsignedShort(buf.readerIndex()) == STR_DOM_ADDR.getValue()) // RS uses a hack to parse hidden addresses, so we do the same :/ + { + // is hidden address + hiddenAddress = (String) deserialize(buf, STR_DOM_ADDR); + hiddenPort = deserializeShort(buf); + } + else + { + // is normal address + localAddressV4 = (PeerAddress) deserialize(buf, ADDRESS); + externalAddressV4 = (PeerAddress) deserialize(buf, ADDRESS); + localAddressV6 = (PeerAddress) deserialize(buf, ADDRESS); + externalAddressV6 = (PeerAddress) deserialize(buf, ADDRESS); + currentConnectAddress = (PeerAddress) deserialize(buf, ADDRESS); + hostname = (String) deserialize(buf, STR_DYNDNS); + + localAddressList = (List) deserialize(buf, ADDRESS_SET); + externalAddressList = (List) deserialize(buf, ADDRESS_SET); + } + } + + public long getPgpIdentifier() + { + return pgpIdentifier; + } + + public LocationId getLocationId() + { + return locationId; + } + + public String getLocationName() + { + return locationName; + } + + public String getVersion() + { + return version; + } + + public NetMode getNetMode() + { + return netMode; + } + + public short getVsDisc() + { + return vsDisc; + } + + public short getVsDht() + { + return vsDht; + } + + public int getLastContact() + { + return lastContact; + } + + public String getHiddenAddress() + { + return hiddenAddress; + } + + public short getHiddenPort() + { + return hiddenPort; + } + + public PeerAddress getLocalAddressV4() + { + return localAddressV4; + } + + public PeerAddress getExternalAddressV4() + { + return externalAddressV4; + } + + public PeerAddress getLocalAddressV6() + { + return localAddressV6; + } + + public PeerAddress getExternalAddressV6() + { + return externalAddressV6; + } + + public PeerAddress getCurrentConnectAddress() + { + return currentConnectAddress; + } + + public String getHostname() + { + return hostname; + } + + public List getLocalAddressList() + { + return localAddressList; + } + + public List getExternalAddressList() + { + return externalAddressList; + } + + @Override + public String toString() + { + return "DiscoveryContactItem{" + + "pgpIdentifier=" + Id.toString(pgpIdentifier) + + ", locationId=" + locationId + + ", location='" + locationName + '\'' + + ", version='" + version + '\'' + + ", netMode=" + netMode + + ", vsDisc=" + vsDisc + + ", vsDht=" + vsDht + + ", lastContact=" + lastContact + + ", hiddenAddress='" + hiddenAddress + '\'' + + ", hiddenPort=" + hiddenPort + + ", localAddressV4=" + localAddressV4 + + ", externalAddressV4=" + externalAddressV4 + + ", localAddressV6=" + localAddressV6 + + ", externalAddressV6=" + externalAddressV6 + + ", currentConnectAddress=" + currentConnectAddress + + ", hostname='" + hostname + '\'' + + ", localAddressList=" + localAddressList + + ", externalAddressList=" + externalAddressList + + '}'; + } + + + public static final class Builder + { + private long pgpIdentifier; + private LocationId locationId; + private String location; + private String version; + private NetMode netMode; + private short vsDisc; + private short vsDht; + private int lastContact; + private String hiddenAddress; + private short hiddenPort; + private PeerAddress localAddressV4; + private PeerAddress externalAddressV4; + private PeerAddress localAddressV6; + private PeerAddress externalAddressV6; + private PeerAddress currentConnectAddress; + private String hostname; + private List localAddressList; + private List externalAddressList; + + private Builder() + { + } + + public Builder setPgpIdentifier(long pgpIdentifier) + { + this.pgpIdentifier = pgpIdentifier; + return this; + } + + public Builder setLocationId(LocationId locationId) + { + this.locationId = locationId; + return this; + } + + public Builder setLocationName(String locationName) + { + this.location = locationName; + return this; + } + + public Builder setVersion(String version) + { + this.version = version; + return this; + } + + public Builder setNetMode(NetMode netMode) + { + this.netMode = netMode; + return this; + } + + public Builder setVsDisc(int vsDisc) + { + this.vsDisc = (short) vsDisc; + return this; + } + + public Builder setVsDht(int vsDht) + { + this.vsDht = (short) vsDht; + return this; + } + + public Builder setLastContact(int lastContact) + { + this.lastContact = lastContact; + return this; + } + + public Builder setHiddenAddress(String hiddenAddress) + { + this.hiddenAddress = hiddenAddress; + return this; + } + + public Builder setHiddenPort(short hiddenPort) + { + this.hiddenPort = hiddenPort; + return this; + } + + public Builder setLocalAddressV4(PeerAddress localAddressV4) + { + this.localAddressV4 = localAddressV4; + return this; + } + + public Builder setExternalAddressV4(PeerAddress externalAddressV4) + { + this.externalAddressV4 = externalAddressV4; + return this; + } + + public Builder setLocalAddressV6(PeerAddress localAddressV6) + { + this.localAddressV6 = localAddressV6; + return this; + } + + public Builder setExternalAddressV6(PeerAddress externalAddressV6) + { + this.externalAddressV6 = externalAddressV6; + return this; + } + + public Builder setCurrentConnectAddress(PeerAddress currentConnectAddress) + { + this.currentConnectAddress = currentConnectAddress; + return this; + } + + public Builder setHostname(String hostname) + { + this.hostname = hostname; + return this; + } + + public Builder setLocalAddressList(List localAddressList) + { + this.localAddressList = localAddressList; + return this; + } + + public Builder setExternalAddressList(List externalAddressList) + { + this.externalAddressList = externalAddressList; + return this; + } + + public DiscoveryContactItem build() + { + return new DiscoveryContactItem(this); + } + } +} diff --git a/app/src/main/java/io/xeres/app/xrs/service/discovery/item/DiscoveryIdentityListItem.java b/app/src/main/java/io/xeres/app/xrs/service/discovery/item/DiscoveryIdentityListItem.java new file mode 100644 index 000000000..c034d46ad --- /dev/null +++ b/app/src/main/java/io/xeres/app/xrs/service/discovery/item/DiscoveryIdentityListItem.java @@ -0,0 +1,58 @@ +/* + * Copyright (c) 2019-2021 by David Gerber - https://zapek.com + * + * This file is part of Xeres. + * + * Xeres is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Xeres is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Xeres. If not, see . + */ + +package io.xeres.app.xrs.service.discovery.item; + +import io.xeres.app.xrs.item.Item; +import io.xeres.app.xrs.serialization.RsSerialized; +import io.xeres.common.id.GxsId; + +import java.util.ArrayList; +import java.util.List; + +import static java.util.stream.Collectors.joining; + +public class DiscoveryIdentityListItem extends Item +{ + @RsSerialized + private final List identities = new ArrayList<>(); + + public DiscoveryIdentityListItem() + { + // Needed for instantiation + } + + public DiscoveryIdentityListItem(List identities) + { + this.identities.addAll(identities); + } + + public List getIdentities() + { + return identities; + } + + @Override + public String toString() + { + return "DiscoveryIdentityListItem{" + + "identities=" + identities.stream().map(Object::toString).collect(joining(", ")) + + '}'; + } +} diff --git a/app/src/main/java/io/xeres/app/xrs/service/discovery/item/DiscoveryPgpKeyItem.java b/app/src/main/java/io/xeres/app/xrs/service/discovery/item/DiscoveryPgpKeyItem.java new file mode 100644 index 000000000..c9a8ea797 --- /dev/null +++ b/app/src/main/java/io/xeres/app/xrs/service/discovery/item/DiscoveryPgpKeyItem.java @@ -0,0 +1,61 @@ +/* + * Copyright (c) 2019-2020 by David Gerber - https://zapek.com + * + * This file is part of Xeres. + * + * Xeres is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Xeres is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Xeres. If not, see . + */ + +package io.xeres.app.xrs.service.discovery.item; + +import io.xeres.app.xrs.item.Item; +import io.xeres.app.xrs.serialization.RsSerialized; +import io.xeres.common.id.Id; + +public class DiscoveryPgpKeyItem extends Item +{ + @RsSerialized + private long pgpIdentifier; + + @RsSerialized + private byte[] keyData; + + public DiscoveryPgpKeyItem() + { + } + + public DiscoveryPgpKeyItem(long pgpIdentifier, byte[] keyData) + { + this.pgpIdentifier = pgpIdentifier; + this.keyData = keyData; + } + + public long getPgpIdentifier() + { + return pgpIdentifier; + } + + public byte[] getKeyData() + { + return keyData; + } + + @Override + public String toString() + { + return "DiscoveryPgpKeyItem{" + + "pgpIdentifier=" + Id.toString(pgpIdentifier) + + '}'; + } +} diff --git a/app/src/main/java/io/xeres/app/xrs/service/discovery/item/DiscoveryPgpListItem.java b/app/src/main/java/io/xeres/app/xrs/service/discovery/item/DiscoveryPgpListItem.java new file mode 100644 index 000000000..3613e11a8 --- /dev/null +++ b/app/src/main/java/io/xeres/app/xrs/service/discovery/item/DiscoveryPgpListItem.java @@ -0,0 +1,94 @@ +/* + * Copyright (c) 2019-2021 by David Gerber - https://zapek.com + * + * This file is part of Xeres. + * + * Xeres is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Xeres is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Xeres. If not, see . + */ + +package io.xeres.app.xrs.service.discovery.item; + +import io.netty.buffer.ByteBuf; +import io.xeres.app.xrs.item.Item; +import io.xeres.app.xrs.serialization.RsSerializable; +import io.xeres.app.xrs.serialization.SerializationFlags; +import io.xeres.common.id.Id; + +import java.util.HashSet; +import java.util.Set; + +import static io.xeres.app.xrs.serialization.Serializer.*; +import static io.xeres.app.xrs.serialization.TlvType.SET_PGP_ID; +import static java.util.stream.Collectors.joining; + +public class DiscoveryPgpListItem extends Item implements RsSerializable +{ + public enum Mode + { + NONE, + FRIENDS, + GET_CERT + } + + private Mode mode; + private Set pgpIds = new HashSet<>(); + + public DiscoveryPgpListItem() + { + + } + + public DiscoveryPgpListItem(Mode mode, Set pgpIds) + { + this.mode = mode; + this.pgpIds = pgpIds; + } + + public Mode getMode() + { + return mode; + } + + public Set getPgpIds() + { + return pgpIds; + } + + @Override + public int writeObject(ByteBuf buf, Set serializationFlags) + { + var size = 0; + + size += serialize(buf, mode); + size += serialize(buf, SET_PGP_ID, pgpIds); + return size; + } + + @Override + @SuppressWarnings("unchecked") + public void readObject(ByteBuf buf, Set serializationFlags) + { + mode = deserializeEnum(buf, Mode.class); + pgpIds = (Set) deserialize(buf, SET_PGP_ID); + } + + @Override + public String toString() + { + return "DiscoveryPgpListItem{" + + "mode=" + mode + + ", pgpIds=" + pgpIds.stream().map(Id::toString).collect(joining(", ")) + + '}'; + } +} diff --git a/app/src/main/java/io/xeres/app/xrs/service/gxs/GxsService.java b/app/src/main/java/io/xeres/app/xrs/service/gxs/GxsService.java new file mode 100644 index 000000000..00d42e416 --- /dev/null +++ b/app/src/main/java/io/xeres/app/xrs/service/gxs/GxsService.java @@ -0,0 +1,233 @@ +/* + * Copyright (c) 2019-2021 by David Gerber - https://zapek.com + * + * This file is part of Xeres. + * + * Xeres is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Xeres is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Xeres. If not, see . + */ + +package io.xeres.app.xrs.service.gxs; + +import io.xeres.app.database.DatabaseSession; +import io.xeres.app.database.DatabaseSessionManager; +import io.xeres.app.database.model.gxs.GxsGroupItem; +import io.xeres.app.database.model.gxs.GxsMessageItem; +import io.xeres.app.net.peer.PeerConnection; +import io.xeres.app.net.peer.PeerConnectionManager; +import io.xeres.app.service.GxsExchangeService; +import io.xeres.app.xrs.item.Item; +import io.xeres.app.xrs.service.RsService; +import io.xeres.app.xrs.service.RsServiceInitPriority; +import io.xeres.app.xrs.service.RsServiceType; +import io.xeres.app.xrs.service.gxs.item.*; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.core.env.Environment; + +import java.time.Duration; +import java.time.Instant; +import java.util.ArrayList; +import java.util.EnumSet; +import java.util.List; +import java.util.Map; +import java.util.concurrent.TimeUnit; + +public abstract class GxsService extends RsService +{ + private static final Logger log = LoggerFactory.getLogger(GxsService.class); + + private static final int KEY_TRANSACTION_ID = 1; + + /** + * When to perform synchronization run with a peer. + */ + private static final Duration SYNCHRONIZATION_DELAY = Duration.ofMinutes(1); + + private final GxsExchangeService gxsExchangeService; + protected final GxsTransactionManager gxsTransactionManager; + private final DatabaseSessionManager databaseSessionManager; + + protected GxsService(Environment environment, PeerConnectionManager peerConnectionManager, GxsExchangeService gxsExchangeService, GxsTransactionManager gxsTransactionManager, DatabaseSessionManager databaseSessionManager) + { + super(environment, peerConnectionManager); + this.gxsExchangeService = gxsExchangeService; + this.gxsTransactionManager = gxsTransactionManager; + this.databaseSessionManager = databaseSessionManager; + } + + /** + * Gets the Gxs implementation of the group. + * + * @return the subclass of the GxsGroupItem + */ + public abstract Class getGroupClass(); + + /** + * Gets the Gxs implementation of the message. + * + * @return the subclass of the GxsMessageItem + */ + public abstract Class getMessageClass(); + + public abstract List getGroups(PeerConnection peerConnection, Instant since); + + public abstract void processItems(PeerConnection peerConnection, List items); + + @Override + public RsServiceType getServiceType() + { + throw new IllegalStateException("Must override getServiceType()"); + } + + @Override + public Map, Integer> getSupportedItems() + { + return Map.of( + GxsSyncGroupRequestItem.class, 1, + GxsSyncGroupItem.class, 2, + GxsSyncGroupStatsItem.class, 3, + GxsTransferGroupItem.class, 4, + GxsTransactionItem.class, 64 + ); + } + + @Override + public RsServiceInitPriority getInitPriority() + { + return RsServiceInitPriority.LOW; + } + + @Override + public void initialize(PeerConnection peerConnection) + { + peerConnection.scheduleWithFixedDelay( + () -> sync(peerConnection), + SYNCHRONIZATION_DELAY.toSeconds(), // XXX: add some randomness to avoid global peer sync? maybe also for chatservice + SYNCHRONIZATION_DELAY.toSeconds(), + TimeUnit.SECONDS + ); + } + + @Override + public void handleItem(PeerConnection peerConnection, Item item) + { + log.debug("Got item: {}", item); + if (item instanceof GxsExchange gxsExchangeItem) + { + if (gxsExchangeItem.getTransactionId() != 0) + { + handleTransaction(peerConnection, gxsExchangeItem); + } + else if (item instanceof GxsSyncGroupRequestItem gxsSyncGroupRequestItem) + { + handleGxsSyncGroupRequestItem(peerConnection, gxsSyncGroupRequestItem); + } + } + else + { + log.error("Not a GxsExchange item: {}, ignoring", item); + } + } + + private void sync(PeerConnection peerConnection) + { + var gxsSyncGroupRequestItem = new GxsSyncGroupRequestItem(gxsExchangeService.getLastPeerUpdate(peerConnection.getLocation(), getServiceType())); + log.debug("Asking peer {} for last local sync {} for service {}", peerConnection, gxsExchangeService.getLastPeerUpdate(peerConnection.getLocation(), getServiceType()), getServiceType()); + writeItem(peerConnection, gxsSyncGroupRequestItem); + } + + // XXX: maybe have some Gxs dedicated methods... + + private void handleGxsSyncGroupRequestItem(PeerConnection peerConnection, GxsSyncGroupRequestItem item) + { + log.debug("Got sync request item: {} from peer {}", item, peerConnection); + + int transactionId = getTransactionId(peerConnection); + Instant since = Instant.ofEpochSecond(item.getUpdateTimestamp()); + if (areGxsUpdatesAvailableForPeer(since)) + { + try (var session = new DatabaseSession(databaseSessionManager)) + { + log.debug("Updates available for peer, sending..."); + List items = new ArrayList<>(); + + // XXX: check if the group is subscribed (subscribeFlags & SUBSCRIBED)... what to do with gxsid? seems subscribe to all groups? + getGroups(peerConnection, since).forEach(gxsGroupItem -> { + log.debug("Adding groupId of item: {}", gxsGroupItem); + if (isGxsAllowedForPeer(peerConnection, gxsGroupItem)) + { + var gxsSyncGroupItem = new GxsSyncGroupItem( + EnumSet.of(SyncFlags.RESPONSE), + gxsGroupItem, + transactionId); + + items.add(gxsSyncGroupItem); + } + }); + // the items are included in a transaction (they all have the same transaction number) + + log.debug("Calling transaction, number of items: {}", items.size()); + gxsTransactionManager.startOutgoingTransaction( + peerConnection, + items, + gxsExchangeService.getLastServiceUpdate(getServiceType()), // XXX: mGrpServerUpdate.grpUpdateTS... I think it's that but recheck + transactionId, + this + ); + } + } + else + { + log.debug("No update available for peer"); // XXX: remove... + } + + // XXX: check if the peer is subscribed, encrypt or not the group, etc... it's rsgxsnetservice.cc/handleRecvSyncGroup we might not need that for gxsid transferts + + // XXX: to handle the synchronization we must know which tables to use, then it's generic + } + + private void handleTransaction(PeerConnection peerConnection, GxsExchange item) + { + if (item instanceof GxsTransactionItem gxsTransactionItem) + { + gxsTransactionManager.processTransaction(peerConnection, gxsTransactionItem, this); + } + else + { + if (gxsTransactionManager.addContent(peerConnection, item, this)) + { + gxsExchangeService.setLastPeerUpdate(peerConnection.getLocation(), getServiceType(), Instant.now()); + } + } + } + + protected int getTransactionId(PeerConnection peerConnection) + { + int transactionId = (int) peerConnection.getServiceData(this, KEY_TRANSACTION_ID).orElse(1); + peerConnection.putServiceData(this, KEY_TRANSACTION_ID, ++transactionId); + return transactionId; + } + + private boolean areGxsUpdatesAvailableForPeer(Instant lastPeerUpdate) + { + log.debug("Comparing our last update: {} to peer's last update: {}", gxsExchangeService.getLastServiceUpdate(getServiceType()), lastPeerUpdate); + // XXX: there should be a way to detect if the peer is sending a lastPeerUpdate several times (means the transaction isn't complete yet) + return lastPeerUpdate.isBefore(gxsExchangeService.getLastServiceUpdate(getServiceType())); + } + + private boolean isGxsAllowedForPeer(PeerConnection peerConnection, GxsGroupItem item) + { + return true; // XXX: later one we should compare with the circles (though that can be done on the SQL request too) + } +} diff --git a/app/src/main/java/io/xeres/app/xrs/service/gxs/GxsTransactionManager.java b/app/src/main/java/io/xeres/app/xrs/service/gxs/GxsTransactionManager.java new file mode 100644 index 000000000..b9cc7a688 --- /dev/null +++ b/app/src/main/java/io/xeres/app/xrs/service/gxs/GxsTransactionManager.java @@ -0,0 +1,213 @@ +/* + * Copyright (c) 2019-2021 by David Gerber - https://zapek.com + * + * This file is part of Xeres. + * + * Xeres is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Xeres is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Xeres. If not, see . + */ + +package io.xeres.app.xrs.service.gxs; + +import io.xeres.app.net.peer.PeerConnection; +import io.xeres.app.net.peer.PeerConnectionManager; +import io.xeres.app.xrs.service.gxs.Transaction.Type; +import io.xeres.app.xrs.service.gxs.item.GxsExchange; +import io.xeres.app.xrs.service.gxs.item.GxsTransactionItem; +import io.xeres.app.xrs.service.gxs.item.TransactionFlags; +import io.xeres.common.id.LocationId; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.stereotype.Service; + +import java.time.Duration; +import java.time.Instant; +import java.util.*; +import java.util.concurrent.ConcurrentHashMap; + +import static io.xeres.app.xrs.service.gxs.Transaction.State; +import static io.xeres.app.xrs.service.gxs.Transaction.createOutgoing; +import static io.xeres.app.xrs.service.gxs.item.TransactionFlags.*; + +@Service +public class GxsTransactionManager +{ + private static final Logger log = LoggerFactory.getLogger(GxsTransactionManager.class); + + private static final Duration TRANSACTION_TIMEOUT = Duration.ofSeconds(2000); + + private final PeerConnectionManager peerConnectionManager; + + private final Map> incomingTransactions = new ConcurrentHashMap<>(); + private final Map> outgoingTransactions = new ConcurrentHashMap<>(); + + public GxsTransactionManager(PeerConnectionManager peerConnectionManager) + { + this.peerConnectionManager = peerConnectionManager; + } + + private void addIncomingTransaction(PeerConnection peerConnection, Transaction transaction) + { + log.debug("Adding incoming transaction for {}", peerConnection); + addTransaction(peerConnection, transaction, incomingTransactions); + } + + private void addOutgoingTransaction(PeerConnection peerConnection, Transaction transaction) + { + log.debug("Adding outgoing transaction for {}", peerConnection); + addTransaction(peerConnection, transaction, outgoingTransactions); + } + + private void addTransaction(PeerConnection peerConnection, Transaction transaction, Map> transactionList) + { + Map transactionMap = transactionList.computeIfAbsent(peerConnection.getLocation().getLocationId(), key -> new HashMap<>()); + if (transactionMap.putIfAbsent(transaction.getId(), transaction) != null) + { + throw new IllegalStateException("Transaction " + transaction.getId() + " for peer " + peerConnection + " already exists. Should not happen (tm)"); + } + } + + private Transaction getTransaction(PeerConnection peerConnection, int id, Type type) + { + var locationId = peerConnection.getLocation().getLocationId(); + + Map transactionMap = type == Type.INCOMING ? incomingTransactions.get(locationId) : outgoingTransactions.get(locationId); + if (transactionMap == null) + { + throw new IllegalStateException("No existing transaction for peer " + peerConnection); + } + Transaction transaction = transactionMap.get(id); + if (transaction == null) + { + throw new IllegalStateException("No existing transaction for peer " + peerConnection); + } + if (transaction.hasTimeout()) + { + throw new IllegalStateException("Transaction timed out for peer " + peerConnection); + } + return transaction; + } + + private void removeTransaction(PeerConnection peerConnection, Transaction transaction) + { + var locationId = peerConnection.getLocation().getLocationId(); + + Map transactionMap = transaction.getType() == Type.INCOMING ? incomingTransactions.get(locationId) : outgoingTransactions.get(locationId); + if (transactionMap == null) + { + throw new IllegalStateException("No existing transaction for removal for peer " + peerConnection); + } + if (!transactionMap.remove(transaction.getId(), transaction)) + { + throw new IllegalStateException("No existing transaction for removal for peer " + peerConnection); + } + // XXX: remove, and possible check if the state is right before doing so (ie. COMPLETED, etc...) + } + + public void startOutgoingTransaction(PeerConnection peerConnection, List items, Instant update, int transactionId, GxsService gxsService) + { + startOutgoingTransaction(peerConnection, EnumSet.of(BEGIN_INCOMING, TYPE_GROUP_LIST_RESPONSE), items, update, transactionId, gxsService); + } + + public void startOutgoingTransactionRequest(PeerConnection peerConnection, List items, int transactionId, GxsService gxsService) + { + startOutgoingTransaction(peerConnection, EnumSet.of(BEGIN_INCOMING, TYPE_GROUP_LIST_REQUEST), items, Instant.EPOCH, transactionId, gxsService); + } + + private void startOutgoingTransaction(PeerConnection peerConnection, Set flags, List items, Instant update, int transactionId, GxsService gxsService) + { + log.debug("Sending transaction (id: {})", transactionId); + var transaction = createOutgoing(transactionId, items, TRANSACTION_TIMEOUT, gxsService); + addOutgoingTransaction(peerConnection, transaction); + + var startTransactionItem = new GxsTransactionItem( + flags, + items.size(), + (int) update.getEpochSecond(), + transactionId); + + peerConnectionManager.writeItem(peerConnection, startTransactionItem, gxsService); + + // XXX: periodically check for the timeout in case the peer doesn't answer anymore + } + + public void processTransaction(PeerConnection peerConnection, GxsTransactionItem item, GxsService gxsService) + { + log.debug("Processing transaction {}", item); + if (item.getFlags().contains(BEGIN_INCOMING)) + { + // This is an incoming connection + log.debug("Incoming transaction, sending back OUTGOING"); + var transaction = Transaction.createIncoming(item.getTransactionId(), item.getItemCount(), TRANSACTION_TIMEOUT, gxsService); + addIncomingTransaction(peerConnection, transaction); + + Set transactionFlags = EnumSet.copyOf(item.getFlags()); + transactionFlags.retainAll(TransactionFlags.ofTypes()); + transactionFlags.add(BEGIN_OUTGOING); + + var readyTransactionItem = new GxsTransactionItem( + transactionFlags, + item.getTransactionId() + ); + peerConnectionManager.writeItem(peerConnection, readyTransactionItem, transaction.getService()); + transaction.setState(State.RECEIVING); + } + else if (item.getFlags().contains(BEGIN_OUTGOING)) + { + // This is the confirmation by the peer of our outgoing connection + log.debug("Outgoing transaction, sending items..."); + var transaction = getTransaction(peerConnection, item.getTransactionId(), Type.OUTGOING); + transaction.setState(State.SENDING); + + log.debug("{} items to go", transaction.getItems().size()); + transaction.getItems().forEach(gxsExchange -> peerConnectionManager.writeItem(peerConnection, gxsExchange, transaction.getService())); + log.debug("done"); + + transaction.setState(State.WAITING_CONFIRMATION); + } + else if (item.getFlags().contains(END_SUCCESS)) + { + // The peer confirms success + log.debug("Got transaction success, removing the transaction"); + var transaction = getTransaction(peerConnection, item.getTransactionId(), Type.OUTGOING); + transaction.setState(State.COMPLETED); + removeTransaction(peerConnection, transaction); + } + } + + public boolean addContent(PeerConnection peerConnection, GxsExchange item, GxsService gxsService) + { + log.debug("Adding transaction content: {}", item); + var transaction = getTransaction(peerConnection, item.getTransactionId(), Type.INCOMING); + transaction.addItem(item); + + if (transaction.hasAllItems()) + { + log.debug("Transaction successful, sending COMPLETED"); + transaction.setState(State.COMPLETED); + var successTransactionItem = new GxsTransactionItem( + EnumSet.of(END_SUCCESS), + transaction.getId() + ); + peerConnectionManager.writeItem(peerConnection, successTransactionItem, transaction.getService()); + + gxsService.processItems(peerConnection, transaction.getItems()); // XXX: how will processItems() know what the items are? should the transaction have something to know that? + + removeTransaction(peerConnection, transaction); + // XXX: in the case that interest us, GxsIdService would call requestGxsGroups() + + return true; + } + return false; + } +} diff --git a/app/src/main/java/io/xeres/app/xrs/service/gxs/Transaction.java b/app/src/main/java/io/xeres/app/xrs/service/gxs/Transaction.java new file mode 100644 index 000000000..8ad1b50eb --- /dev/null +++ b/app/src/main/java/io/xeres/app/xrs/service/gxs/Transaction.java @@ -0,0 +1,132 @@ +/* + * Copyright (c) 2019-2021 by David Gerber - https://zapek.com + * + * This file is part of Xeres. + * + * Xeres is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Xeres is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Xeres. If not, see . + */ + +package io.xeres.app.xrs.service.gxs; + +import io.xeres.app.xrs.service.RsService; +import io.xeres.app.xrs.service.gxs.item.GxsExchange; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.time.Duration; +import java.time.Instant; +import java.util.ArrayList; +import java.util.List; + +public final class Transaction +{ + private static final Logger log = LoggerFactory.getLogger(Transaction.class); + + public enum State + { + STARTING, + RECEIVING, + SENDING, + COMPLETED, + FAILED, + WAITING_CONFIRMATION + } + + public enum Type + { + INCOMING, + OUTGOING + } + + private final int id; + private State state; + private final Type type; + private final Instant start; + private final Duration timeout; + private final List items; + private final int itemCount; + private final GxsService service; + + public static Transaction createOutgoing(int id, List items, Duration timeout, GxsService service) + { + return new Transaction(id, items, items.size(), timeout, service, State.WAITING_CONFIRMATION, Type.OUTGOING); + } + + public static Transaction createIncoming(int id, int itemCount, Duration timeout, GxsService service) + { + return new Transaction(id, new ArrayList<>(), itemCount, timeout, service, State.STARTING, Type.INCOMING); + } + + private Transaction(int id, List items, int itemCount, Duration timeout, GxsService service, State state, Type type) + { + if (itemCount == 0) + { + throw new IllegalArgumentException("Can't create an empty transaction"); + } + this.id = id; + this.items = items; + this.itemCount = itemCount; + this.timeout = timeout; + this.service = service; + this.state = state; + this.type = type; + this.start = Instant.now(); + } + + public int getId() + { + return id; + } + + public State getState() + { + return state; + } + + public Type getType() + { + return type; + } + + public void setState(State state) + { + this.state = state; + } + + public List getItems() + { + return items; + } + + public void addItem(GxsExchange item) + { + items.add(item); + } + + public RsService getService() + { + return service; + } + + public boolean hasAllItems() + { + log.debug("itemcount: {}, current items size: {}", itemCount, items.size()); + return itemCount == items.size(); + } + + public boolean hasTimeout() + { + return start.plus(timeout).isBefore(Instant.now()); + } +} diff --git a/app/src/main/java/io/xeres/app/xrs/service/gxs/item/GxsExchange.java b/app/src/main/java/io/xeres/app/xrs/service/gxs/item/GxsExchange.java new file mode 100644 index 000000000..c2d3ef007 --- /dev/null +++ b/app/src/main/java/io/xeres/app/xrs/service/gxs/item/GxsExchange.java @@ -0,0 +1,54 @@ +/* + * Copyright (c) 2019-2021 by David Gerber - https://zapek.com + * + * This file is part of Xeres. + * + * Xeres is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Xeres is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Xeres. If not, see . + */ + +package io.xeres.app.xrs.service.gxs.item; + +import io.xeres.app.xrs.item.Item; +import io.xeres.app.xrs.item.ItemPriority; +import io.xeres.app.xrs.serialization.RsSerialized; + +public abstract class GxsExchange extends Item +{ + @RsSerialized + private int transactionId; + + @Override + public int getPriority() + { + return ItemPriority.GXS.getPriority(); + } + + public int getTransactionId() + { + return transactionId; + } + + public void setTransactionId(int transactionId) + { + this.transactionId = transactionId; + } + + @Override + public String toString() + { + return "GxsExchange{" + + "transactionId=" + transactionId + + '}'; + } +} diff --git a/app/src/main/java/io/xeres/app/xrs/service/gxs/item/GxsSyncGroupItem.java b/app/src/main/java/io/xeres/app/xrs/service/gxs/item/GxsSyncGroupItem.java new file mode 100644 index 000000000..11b21478c --- /dev/null +++ b/app/src/main/java/io/xeres/app/xrs/service/gxs/item/GxsSyncGroupItem.java @@ -0,0 +1,82 @@ +/* + * Copyright (c) 2019-2021 by David Gerber - https://zapek.com + * + * This file is part of Xeres. + * + * Xeres is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Xeres is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Xeres. If not, see . + */ + +package io.xeres.app.xrs.service.gxs.item; + +import io.xeres.app.database.model.gxs.GxsGroupItem; +import io.xeres.app.xrs.serialization.FieldSize; +import io.xeres.app.xrs.serialization.RsSerialized; +import io.xeres.common.id.GxsId; + +import java.util.Set; + +/** + * Item used to send the list of groups to a peer. + */ +public class GxsSyncGroupItem extends GxsExchange +{ + @RsSerialized(fieldSize = FieldSize.BYTE) + private Set flags; + + @RsSerialized + private GxsId groupId; + + @RsSerialized + private int publishTimestamp; + + @RsSerialized + private GxsId authorId; + + public GxsSyncGroupItem() + { + // Needed + } + + public GxsSyncGroupItem(Set flags, GxsGroupItem groupItem, int transactionId) + { + this.flags = flags; + this.publishTimestamp = (int) groupItem.getPublished().getEpochSecond(); + this.groupId = groupItem.getGxsId(); + this.authorId = groupItem.getAuthor(); + setTransactionId(transactionId); + } + + public GxsSyncGroupItem(Set flags, GxsId groupId, int transactionId) + { + this.flags = flags; + this.groupId = groupId; + setTransactionId(transactionId); + } + + public GxsId getGroupId() + { + return groupId; + } + + @Override + public String toString() + { + return "GxsSyncGroupItem{" + + "flags=" + flags + + ", publishTimestamp=" + publishTimestamp + + ", groupId=" + groupId + + ", authorId=" + authorId + + '}'; + } +} diff --git a/app/src/main/java/io/xeres/app/xrs/service/gxs/item/GxsSyncGroupRequestItem.java b/app/src/main/java/io/xeres/app/xrs/service/gxs/item/GxsSyncGroupRequestItem.java new file mode 100644 index 000000000..ebba80457 --- /dev/null +++ b/app/src/main/java/io/xeres/app/xrs/service/gxs/item/GxsSyncGroupRequestItem.java @@ -0,0 +1,86 @@ +/* + * Copyright (c) 2019-2021 by David Gerber - https://zapek.com + * + * This file is part of Xeres. + * + * Xeres is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Xeres is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Xeres. If not, see . + */ + +package io.xeres.app.xrs.service.gxs.item; + +import io.xeres.app.xrs.serialization.RsSerialized; + +import java.time.Instant; + +import static io.xeres.app.xrs.serialization.TlvType.STR_HASH_SHA1; + +/** + * Item used to request group list from a peer. + */ +public class GxsSyncGroupRequestItem extends GxsExchange +{ + @RsSerialized + private byte flag; // unused + + @RsSerialized + private int createdSince; // how far back to sync data + + @RsSerialized(tlvType = STR_HASH_SHA1) + private String syncHash; // unused. This is old stuff where it used to transfer files instead of building tunnels + + @RsSerialized + private int updateTimestamp; // last group update + + public GxsSyncGroupRequestItem() + { + // Needed + } + + public GxsSyncGroupRequestItem(Instant lastUpdate) + { + this.updateTimestamp = (int) lastUpdate.getEpochSecond(); + } + + public int getCreatedSince() + { + return createdSince; + } + + public void setCreatedSince(int createdSince) + { + this.createdSince = createdSince; + } + + public int getUpdateTimestamp() + { + return updateTimestamp; + } + + public void setUpdateTimestamp(int updateTimestamp) + { + this.updateTimestamp = updateTimestamp; + } + + @Override + public String toString() + { + return "GxsSyncGroupRequestItem{" + + ", flag=" + flag + + ", createdSince=" + createdSince + + ", syncHash='" + syncHash + '\'' + + ", updateTimestamp=" + updateTimestamp + + ", super=" + super.toString() + + '}'; + } +} diff --git a/app/src/main/java/io/xeres/app/xrs/service/gxs/item/GxsSyncGroupStatsItem.java b/app/src/main/java/io/xeres/app/xrs/service/gxs/item/GxsSyncGroupStatsItem.java new file mode 100644 index 000000000..cb3b215a3 --- /dev/null +++ b/app/src/main/java/io/xeres/app/xrs/service/gxs/item/GxsSyncGroupStatsItem.java @@ -0,0 +1,57 @@ +/* + * Copyright (c) 2019-2021 by David Gerber - https://zapek.com + * + * This file is part of Xeres. + * + * Xeres is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Xeres is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Xeres. If not, see . + */ + +package io.xeres.app.xrs.service.gxs.item; + +import io.xeres.app.xrs.serialization.RsSerialized; +import io.xeres.common.id.GxsId; + +/** + * This item is used to request statistics about a group. + */ +public class GxsSyncGroupStatsItem extends GxsExchange +{ + @RsSerialized + private RequestType requestType; + + @RsSerialized + private GxsId groupId; + + @RsSerialized + private int numberOfPosts; + + @RsSerialized + private int lastPostTimestamp; + + public GxsSyncGroupStatsItem() + { + // Needed + } + + @Override + public String toString() + { + return "GxsSyncGroupStatsItem{" + + "requestType=" + requestType + + ", groupId=" + groupId + + ", numberOfPosts=" + numberOfPosts + + ", lastPostTimestamp=" + lastPostTimestamp + + '}'; + } +} diff --git a/app/src/main/java/io/xeres/app/xrs/service/gxs/item/GxsTransactionItem.java b/app/src/main/java/io/xeres/app/xrs/service/gxs/item/GxsTransactionItem.java new file mode 100644 index 000000000..60d08c616 --- /dev/null +++ b/app/src/main/java/io/xeres/app/xrs/service/gxs/item/GxsTransactionItem.java @@ -0,0 +1,93 @@ +/* + * Copyright (c) 2019-2021 by David Gerber - https://zapek.com + * + * This file is part of Xeres. + * + * Xeres is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Xeres is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Xeres. If not, see . + */ + +package io.xeres.app.xrs.service.gxs.item; + +import io.xeres.app.xrs.serialization.FieldSize; +import io.xeres.app.xrs.serialization.RsSerialized; + +import java.util.Set; + +/** + * This item is used to make a transaction, which guarantees + * that a collection of items have been received. + */ +public class GxsTransactionItem extends GxsExchange +{ + @RsSerialized(fieldSize = FieldSize.SHORT) + private Set flags; + + @RsSerialized + private int itemCount; + + @RsSerialized + private int updateTimestamp; + + private int timestamp; // Not serialized, used for timeout detection (XXX: I don't think I need it) + + public GxsTransactionItem() + { + // Needed + } + + public GxsTransactionItem(Set flags, int itemCount, int updateTimestamp, int transactionId) + { + this.flags = flags; + this.itemCount = itemCount; + this.updateTimestamp = updateTimestamp; + setTransactionId(transactionId); + } + + public GxsTransactionItem(Set flags, int transactionId) + { + this.flags = flags; + setTransactionId(transactionId); + } + + public Set getFlags() + { + return flags; + } + + public int getItemCount() + { + return itemCount; + } + + public int getUpdateTimestamp() + { + return updateTimestamp; + } + + public int getTimestamp() + { + return timestamp; + } + + @Override + public String toString() + { + return "GxsTransactionItem{" + + "transactionFlag=" + flags + + ", itemCount=" + itemCount + + ", updateTimestamp=" + updateTimestamp + + ", super=" + super.toString() + + '}'; + } +} diff --git a/app/src/main/java/io/xeres/app/xrs/service/gxs/item/GxsTransferGroupItem.java b/app/src/main/java/io/xeres/app/xrs/service/gxs/item/GxsTransferGroupItem.java new file mode 100644 index 000000000..55a10d612 --- /dev/null +++ b/app/src/main/java/io/xeres/app/xrs/service/gxs/item/GxsTransferGroupItem.java @@ -0,0 +1,105 @@ +/* + * Copyright (c) 2019-2021 by David Gerber - https://zapek.com + * + * This file is part of Xeres. + * + * Xeres is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Xeres is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Xeres. If not, see . + */ + +package io.xeres.app.xrs.service.gxs.item; + +import io.netty.buffer.ByteBuf; +import io.xeres.app.xrs.serialization.RsSerializable; +import io.xeres.app.xrs.serialization.SerializationFlags; +import io.xeres.app.xrs.serialization.Serializer; +import io.xeres.common.id.GxsId; + +import java.util.Set; + +/** + * This is used to transfer group data within transactions. This is usually + * backed by a GxsGroupItem which can serialize directly when not used in transactions. + */ +public class GxsTransferGroupItem extends GxsExchange implements RsSerializable +{ + private byte position; // used for splitting up groups + private GxsId groupId; + private byte[] group; // actual group data (I think this is the serialization of GxsGroupItem, the service specific data (ie. avatar, etc...)) + private byte[] meta; // Binary data for the group meta that is sent to friends. should not contain any private key part. this seems to be the serialization of GxsGroupItem + + // XXX: RS also uses a RsGxsGrpMetaData metaData which is the deserialized metadata (may contain private key parts). basically RS juggles with various copies + + public GxsTransferGroupItem() + { + // Needed + } + + public GxsTransferGroupItem(GxsId groupId, byte[] group, byte[] meta, int transactionId) + { + this.groupId = groupId; + this.group = group; + this.meta = meta; + setTransactionId(transactionId); + } + + public byte getPosition() + { + return position; + } + + public GxsId getGroupId() + { + return groupId; + } + + public byte[] getGroup() + { + return group; + } + + public byte[] getMeta() + { + return meta; + } + + @Override + public int writeObject(ByteBuf buf, Set serializationFlags) + { + var size = Serializer.serialize(buf, getTransactionId()); + size += Serializer.serialize(buf, position); + size += Serializer.serialize(buf, groupId); + size += Serializer.serializeTlvBinary(buf, getService().getServiceType().getType(), group); + size += Serializer.serializeTlvBinary(buf, getService().getServiceType().getType(), meta); + return size; + } + + @Override + public void readObject(ByteBuf buf, Set serializationFlags) + { + setTransactionId(Serializer.deserializeInt(buf)); + position = Serializer.deserializeByte(buf); + groupId = (GxsId) Serializer.deserializeIdentifier(buf, GxsId.class); + group = Serializer.deserializeTlvBinary(buf, getService().getServiceType().getType()); + meta = Serializer.deserializeTlvBinary(buf, getService().getServiceType().getType()); + } + + @Override + public String toString() + { + return "GxsTransferGroupItem{" + + "position=" + position + + ", groupId=" + groupId + + '}'; + } +} diff --git a/app/src/main/java/io/xeres/app/xrs/service/gxs/item/RequestType.java b/app/src/main/java/io/xeres/app/xrs/service/gxs/item/RequestType.java new file mode 100644 index 000000000..2d62c6c40 --- /dev/null +++ b/app/src/main/java/io/xeres/app/xrs/service/gxs/item/RequestType.java @@ -0,0 +1,27 @@ +/* + * Copyright (c) 2019-2021 by David Gerber - https://zapek.com + * + * This file is part of Xeres. + * + * Xeres is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Xeres is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Xeres. If not, see . + */ + +package io.xeres.app.xrs.service.gxs.item; + +public enum RequestType +{ + NONE, // Unused + REQUEST, + RESPONSE +} diff --git a/app/src/main/java/io/xeres/app/xrs/service/gxs/item/SyncFlags.java b/app/src/main/java/io/xeres/app/xrs/service/gxs/item/SyncFlags.java new file mode 100644 index 000000000..3f76f7cc9 --- /dev/null +++ b/app/src/main/java/io/xeres/app/xrs/service/gxs/item/SyncFlags.java @@ -0,0 +1,26 @@ +/* + * Copyright (c) 2019-2021 by David Gerber - https://zapek.com + * + * This file is part of Xeres. + * + * Xeres is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Xeres is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Xeres. If not, see . + */ + +package io.xeres.app.xrs.service.gxs.item; + +public enum SyncFlags +{ + REQUEST, + RESPONSE +} diff --git a/app/src/main/java/io/xeres/app/xrs/service/gxs/item/TransactionFlags.java b/app/src/main/java/io/xeres/app/xrs/service/gxs/item/TransactionFlags.java new file mode 100644 index 000000000..b3459dda4 --- /dev/null +++ b/app/src/main/java/io/xeres/app/xrs/service/gxs/item/TransactionFlags.java @@ -0,0 +1,69 @@ +/* + * Copyright (c) 2019-2021 by David Gerber - https://zapek.com + * + * This file is part of Xeres. + * + * Xeres is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Xeres is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Xeres. If not, see . + */ + +package io.xeres.app.xrs.service.gxs.item; + +import java.util.EnumSet; +import java.util.Set; + +public enum TransactionFlags +{ + // States + BEGIN_INCOMING, // FLAG_BEGIN_P1 + BEGIN_OUTGOING, // FLAG_BEGIN_P2 + END_SUCCESS, // FLAG_END_SUCCESS + CANCEL, // FLAG_CANCEL (not used it seems) + END_FAIL_NUM, // FLAG_END_FAIL_NUM (not used it seems) + END_FAIL_TIMEOUT, // FLAG_END_FAIL_TIMEOUT (not used it seems) + END_FAIL_FULL, // FLAG_END_FAIL_FULL (not used it seems) + UNUSED, + // Types + TYPE_GROUP_LIST_RESPONSE, // FLAG_TYPE_GRP_LIST_RESP + TYPE_MESSAGE_LIST_RESPONSE, // FLAG_TYPE_MSG_LIST_RESP + TYPE_GROUP_LIST_REQUEST, // FLAG_TYPE_GRP_LIST_REQ + TYPE_MESSAGE_LIST_REQUEST, // FLAG_TYPE_MSG_LIST_REQ + TYPE_GROUPS, // FLAG_TYPE_GRPS + TYPE_MESSAGES, // FLAG_TYPE_MESSAGES + TYPE_ENCRYPTED_DATA; // FLAG_TYPE_ENCRYPTED_DATA (not used it seems) + + public static Set ofStates() + { + return EnumSet.of( + BEGIN_INCOMING, + BEGIN_OUTGOING, + END_SUCCESS, + CANCEL, + END_FAIL_NUM, + END_FAIL_TIMEOUT, + END_FAIL_FULL + ); + } + + public static Set ofTypes() + { + return EnumSet.of( + TYPE_GROUP_LIST_RESPONSE, + TYPE_MESSAGE_LIST_RESPONSE, + TYPE_GROUP_LIST_REQUEST, + TYPE_MESSAGE_LIST_REQUEST, + TYPE_GROUPS, + TYPE_MESSAGES, + TYPE_ENCRYPTED_DATA); + } +} diff --git a/app/src/main/java/io/xeres/app/xrs/service/gxsid/GxsIdService.java b/app/src/main/java/io/xeres/app/xrs/service/gxsid/GxsIdService.java new file mode 100644 index 000000000..0da4fce1a --- /dev/null +++ b/app/src/main/java/io/xeres/app/xrs/service/gxsid/GxsIdService.java @@ -0,0 +1,234 @@ +/* + * Copyright (c) 2019-2021 by David Gerber - https://zapek.com + * + * This file is part of Xeres. + * + * Xeres is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Xeres is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Xeres. If not, see . + */ + +package io.xeres.app.xrs.service.gxsid; + +import io.netty.buffer.ByteBuf; +import io.netty.buffer.Unpooled; +import io.xeres.app.crypto.rsa.RSA; +import io.xeres.app.database.DatabaseSession; +import io.xeres.app.database.DatabaseSessionManager; +import io.xeres.app.database.model.gxs.GxsGroupItem; +import io.xeres.app.database.model.gxs.GxsMessageItem; +import io.xeres.app.database.model.identity.Identity; +import io.xeres.app.net.peer.PeerConnection; +import io.xeres.app.net.peer.PeerConnectionManager; +import io.xeres.app.service.GxsExchangeService; +import io.xeres.app.service.IdentityService; +import io.xeres.app.xrs.item.Item; +import io.xeres.app.xrs.serialization.RsSerializable; +import io.xeres.app.xrs.serialization.SerializationFlags; +import io.xeres.app.xrs.service.RsServiceType; +import io.xeres.app.xrs.service.gxs.GxsService; +import io.xeres.app.xrs.service.gxs.GxsTransactionManager; +import io.xeres.app.xrs.service.gxs.item.GxsExchange; +import io.xeres.app.xrs.service.gxs.item.GxsSyncGroupItem; +import io.xeres.app.xrs.service.gxs.item.GxsTransferGroupItem; +import io.xeres.app.xrs.service.gxs.item.SyncFlags; +import io.xeres.app.xrs.service.gxsid.item.GxsIdGroupItem; +import io.xeres.common.id.GxsId; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.core.env.Environment; +import org.springframework.stereotype.Component; + +import java.security.NoSuchAlgorithmException; +import java.security.spec.InvalidKeySpecException; +import java.time.Instant; +import java.util.ArrayList; +import java.util.EnumSet; +import java.util.List; +import java.util.Map; + +import static io.xeres.app.xrs.service.RsServiceType.GXSID; + +@Component +public class GxsIdService extends GxsService +{ + private static final Logger log = LoggerFactory.getLogger(GxsIdService.class); + + private final IdentityService identityService; + private final DatabaseSessionManager databaseSessionManager; + + public GxsIdService(Environment environment, PeerConnectionManager peerConnectionManager, GxsExchangeService gxsExchangeService, GxsTransactionManager gxsTransactionManager, DatabaseSessionManager databaseSessionManager, IdentityService identityService) + { + super(environment, peerConnectionManager, gxsExchangeService, gxsTransactionManager, databaseSessionManager); + this.identityService = identityService; + this.databaseSessionManager = databaseSessionManager; + } + + @Override + public Class getGroupClass() + { + return GxsIdGroupItem.class; + } + + @Override + public Class getMessageClass() + { + return null; // We don't use messages + } + + @Override + public RsServiceType getServiceType() + { + return GXSID; + } + + // XXX: for now only GxsService handles the items... GxsIdGroupItem is not used I think (it has the same subtype as GxsSyncGroupItem which means they clash). also disabled handleItem() below + @Override + public Map, Integer> getSupportedItems() + { + return super.getSupportedItems(); +// return Stream.concat(Map.of( +// GxsIdGroupItem.class, 2 +// ).entrySet().stream(), super.getSupportedItems().entrySet().stream()) +// .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue)); + } + + @Override + public List getGroups(PeerConnection peerConnection, Instant since) + { + // XXX: use identityService to return the identities we need. for now we just return ours + return List.of(identityService.getOwnIdentity().getGxsIdGroupItem()); + } + + @Override + public void handleItem(PeerConnection peerConnection, Item item) + { +// if (item instanceof GxsIdGroupItem gxsIdGroupItem) +// { +// handleGxsIdGroupItem(peerConnection, gxsIdGroupItem); +// } + super.handleItem(peerConnection, item); + } + + private void handleGxsIdGroupItem(PeerConnection peerConnection, GxsIdGroupItem item) + { + log.debug("got item: {}", item); + // XXX: I think those only exist when doing a transfer between distant peers + } + + @SuppressWarnings("unchecked") + @Override + public void processItems(PeerConnection peerConnection, List items) + { + if (items.get(0) instanceof GxsSyncGroupItem) // XXX: this is not very nice. maybe I should have some transaction type or so + { + var gxsIds = ((List) items).stream().map(GxsSyncGroupItem::getGroupId).toList(); + log.debug("Peer wants the following gxs ids: {}", gxsIds); + // XXX: for now we just send back our own identity + try (var session = new DatabaseSession(databaseSessionManager)) + { + Identity ownIdentity = identityService.getOwnIdentity(); + if (gxsIds.size() == 1 && gxsIds.get(0).equals(ownIdentity.getGxsIdGroupItem().getGxsId())) + { + sendGxsGroups(peerConnection, List.of(ownIdentity.getGxsIdGroupItem())); + } + else + { + throw new IllegalArgumentException("Requested an ID that we don't have"); + } + } + } + else if (items.get(0) instanceof GxsTransferGroupItem) + { + ((List) items).forEach(item -> { + log.debug("Saving id {}", item.getGroupId()); + + var buf = Unpooled.copiedBuffer(item.getMeta(), item.getGroup()); //XXX: use ctx().alloc()? + var gxsIdGroupItem = new GxsIdGroupItem(); + ((RsSerializable) gxsIdGroupItem).readObject(buf, EnumSet.noneOf(SerializationFlags.class)); // XXX: should we add some helper method into Serializer()? + buf.release(); + + var identity = Identity.createIdentity(gxsIdGroupItem); // XXX: find out if it's a friend. how? + identityService.saveIdentity(identity); + }); + } + } + + // XXX: maybe this should be in GxsService. also other methods I think (though there are gxsIds... hmm... what a mess) + public void requestGxsGroups(PeerConnection peerConnection, List ids) // XXX: maybe use a future to know when the group arrived? it's possible by keeping a list of transactionIds then answering once the answer comes back + { + int transactionId = getTransactionId(peerConnection); + List items = new ArrayList<>(); + + ids.forEach(gxsId -> items.add(new GxsSyncGroupItem(EnumSet.of(SyncFlags.REQUEST), gxsId, transactionId))); + + gxsTransactionManager.startOutgoingTransactionRequest(peerConnection, items, transactionId, this); + } + + public void sendGxsGroups(PeerConnection peerConnection, List gxsGroupItems) + { + int transactionId = getTransactionId(peerConnection); + List items = new ArrayList<>(); + gxsGroupItems.forEach(gxsGroupItem -> { + signGroup(gxsGroupItem); + var groupBuf = Unpooled.buffer(); // XXX: size... well, it autogrows + log.debug("Writing group buf"); + gxsGroupItem.writeObject(groupBuf, EnumSet.of(SerializationFlags.SUBCLASS_ONLY)); + var metaBuf = Unpooled.buffer(); // XXX: size... autogrows as well + log.debug("Writing meta buf"); + gxsGroupItem.writeObject(metaBuf, EnumSet.of(SerializationFlags.SUPERCLASS_ONLY)); + var gxsTransferGroupItem = new GxsTransferGroupItem(gxsGroupItem.getGxsId(), getArray(groupBuf), getArray(metaBuf), transactionId); + gxsTransferGroupItem.setService(this); // XXX: maybe move that on the constructor? since it's kinda needed + items.add(gxsTransferGroupItem); + }); + + gxsTransactionManager.startOutgoingTransaction( + peerConnection, + items, + Instant.now(), // XXX: not sure about that one... recheck + transactionId, + this + ); + } + + private static byte[] getArray(ByteBuf buf) + { + var out = new byte[buf.writerIndex()]; + buf.readBytes(out); + return out; + } + + private static void signGroup(GxsGroupItem gxsGroupItem) + { + var data = serializeItemForSignature(gxsGroupItem); + byte[] signature; + try + { + signature = RSA.sign(data, RSA.getPrivateKey(gxsGroupItem.getAdminPrivateKeyData())); + } + catch (NoSuchAlgorithmException | InvalidKeySpecException e) + { + throw new IllegalArgumentException("Error in private key: " + e.getMessage()); + } + gxsGroupItem.setSignature(signature); + } + + private static byte[] serializeItemForSignature(Item item) + { + item.setOutgoing(Unpooled.buffer().alloc(), 2, RsServiceType.GXSID, 1); // XXX: not very nice to have those arguments hardcoded here + var buf = item.serializeItem(EnumSet.of(SerializationFlags.SIGNATURE)).getBuffer(); + var data = new byte[buf.writerIndex()]; + buf.getBytes(0, data); + buf.release(); + return data; + } +} diff --git a/app/src/main/java/io/xeres/app/xrs/service/gxsid/item/GxsIdGroupItem.java b/app/src/main/java/io/xeres/app/xrs/service/gxsid/item/GxsIdGroupItem.java new file mode 100644 index 000000000..8a805118f --- /dev/null +++ b/app/src/main/java/io/xeres/app/xrs/service/gxsid/item/GxsIdGroupItem.java @@ -0,0 +1,160 @@ +/* + * Copyright (c) 2019-2021 by David Gerber - https://zapek.com + * + * This file is part of Xeres. + * + * Xeres is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Xeres is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Xeres. If not, see . + */ + +package io.xeres.app.xrs.service.gxsid.item; + +import io.netty.buffer.ByteBuf; +import io.xeres.app.database.model.gxs.GxsGroupItem; +import io.xeres.app.xrs.serialization.RsSerializable; +import io.xeres.app.xrs.serialization.SerializationFlags; +import io.xeres.app.xrs.serialization.TlvType; +import io.xeres.common.id.GxsId; +import io.xeres.common.id.Sha1Sum; + +import javax.persistence.*; +import java.util.ArrayList; +import java.util.List; +import java.util.Set; + +import static io.xeres.app.xrs.serialization.Serializer.*; + +@Entity(name = "gxs_id_groups") +public class GxsIdGroupItem extends GxsGroupItem implements RsSerializable // XXX: beware because we need to be able to serialize just the group data (here) and the group metadata (superclass) +{ + @Embedded + @AttributeOverride(name = "identifier", column = @Column(name = "profile_hash")) + private Sha1Sum profileHash; // hash of the gxsId + public key + private byte[] profileSignature; // XXX: warning, RS puts this in a string! we might have to do some serialization trickery... see p3idservice.cc in service_createGroup(), but I think my system's flexibility makes up for it + + @Transient + private List recognitionTags = new ArrayList<>(); // not used (but serialized) + + // XXX: add avatar image (TlvImage)... the avatar image is optional so we can just ignore it for now + // and it checks if an image is here by checking the size... sigh! will have to use custom serialization + + public GxsIdGroupItem() + { + } + + public GxsIdGroupItem(GxsId gxsId, String name) + { + setGxsId(gxsId); + setName(name); + } + + public Sha1Sum getProfileHash() + { + return profileHash; + } + + public void setProfileHash(Sha1Sum profileHash) + { + this.profileHash = profileHash; + } + + public byte[] getProfileSignature() + { + return profileSignature; + } + + public void setProfileSignature(byte[] profileSignature) + { + this.profileSignature = profileSignature; + } + + @Override + public int writeObject(ByteBuf buf, Set serializationFlags) + { + int size = 0; + + if (serializationFlags.contains(SerializationFlags.SUBCLASS_ONLY)) + { + size += writeObject(buf, 0); + } + else if (serializationFlags.contains(SerializationFlags.SUPERCLASS_ONLY)) + { + return super.writeObject(buf, serializationFlags); + } + else + { + size += super.writeObject(buf, serializationFlags); + size += writeObject(buf, size); + } + return size; + } + + private int writeObject(ByteBuf buf, int parentSize) + { + int size = 0; + + size += serialize(buf, (byte) 2); + size += serialize(buf, (short) 0x211); + size += serialize(buf, (byte) 2); + int sizeOffset = buf.writerIndex(); + size += serialize(buf, 0); // write size at end + + size += serialize(buf, profileHash); + size += serialize(buf, TlvType.STR_SIGN, profileSignature); + size += serialize(buf, TlvType.SET_RECOGN, recognitionTags); + + // XXX: missing avatar image here + + buf.setInt(sizeOffset, size); // write total size + + return size; + } + + @Override + public void readObject(ByteBuf buf, Set serializationFlags) + { + if (serializationFlags.contains(SerializationFlags.SUBCLASS_ONLY)) + { + readObject(buf); + } + else if (serializationFlags.contains(SerializationFlags.SUPERCLASS_ONLY)) + { + super.readObject(buf, serializationFlags); + } + else + { + super.readObject(buf, serializationFlags); + readObject(buf); + } + } + + @SuppressWarnings("unchecked") + private void readObject(ByteBuf buf) + { + // XXX: we have to read the following but... shouldn't there be something else to do it? + buf.readByte(); // 0x2 (packet version) + buf.readShort(); // 0x0211 (service: gxsId) + buf.readByte(); // 0x2 (packet subtype?) + buf.readInt(); // size + + profileHash = (Sha1Sum) deserializeIdentifier(buf, Sha1Sum.class); + profileSignature = (byte[]) deserialize(buf, TlvType.STR_SIGN); + recognitionTags = (List) deserialize(buf, TlvType.SET_RECOGN); + + if (buf.isReadable()) // XXX: I think this works if there's more data to read + { + // XXX: read the avatar image, which is a RsTlvImage (type: 0x1060), like: 1060 00000010 00000000013000000006 + buf.discardReadBytes(); // XXX! + } + } +} diff --git a/app/src/main/java/io/xeres/app/xrs/service/gxsid/item/GxsIdLocalInfoItem.java b/app/src/main/java/io/xeres/app/xrs/service/gxsid/item/GxsIdLocalInfoItem.java new file mode 100644 index 000000000..ddf962cb4 --- /dev/null +++ b/app/src/main/java/io/xeres/app/xrs/service/gxsid/item/GxsIdLocalInfoItem.java @@ -0,0 +1,33 @@ +/* + * Copyright (c) 2019-2021 by David Gerber - https://zapek.com + * + * This file is part of Xeres. + * + * Xeres is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Xeres is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Xeres. If not, see . + */ + +package io.xeres.app.xrs.service.gxsid.item; + +import io.xeres.app.xrs.item.Item; +import io.xeres.common.id.GxsId; + +import java.util.Map; +import java.util.Set; + +// XXX: I marked that as extending GxsGroupItem... why?! there can only be one type... find out +public class GxsIdLocalInfoItem extends Item +{ + private Map timestamps; + private Set contacts; // XXX: huho... we don't handle those, just enumsets... +} diff --git a/app/src/main/java/io/xeres/app/xrs/service/heartbeat/HeartbeatService.java b/app/src/main/java/io/xeres/app/xrs/service/heartbeat/HeartbeatService.java new file mode 100644 index 000000000..0cbaebded --- /dev/null +++ b/app/src/main/java/io/xeres/app/xrs/service/heartbeat/HeartbeatService.java @@ -0,0 +1,77 @@ +/* + * Copyright (c) 2019-2020 by David Gerber - https://zapek.com + * + * This file is part of Xeres. + * + * Xeres is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Xeres is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Xeres. If not, see . + */ + +package io.xeres.app.xrs.service.heartbeat; + +import io.xeres.app.net.peer.PeerConnection; +import io.xeres.app.net.peer.PeerConnectionManager; +import io.xeres.app.xrs.item.Item; +import io.xeres.app.xrs.service.RsService; +import io.xeres.app.xrs.service.RsServiceInitPriority; +import io.xeres.app.xrs.service.RsServiceType; +import io.xeres.app.xrs.service.heartbeat.item.HeartbeatItem; +import org.springframework.core.env.Environment; +import org.springframework.stereotype.Component; + +import java.util.Map; +import java.util.concurrent.TimeUnit; + +import static io.xeres.app.xrs.service.RsServiceType.HEARTBEAT; + +@Component +public class HeartbeatService extends RsService +{ + public HeartbeatService(Environment environment, PeerConnectionManager peerConnectionManager) + { + super(environment, peerConnectionManager); + } + + @Override + public RsServiceType getServiceType() + { + return HEARTBEAT; + } + + @Override + public Map, Integer> getSupportedItems() + { + return Map.of(HeartbeatItem.class, 1); + } + + @Override + public RsServiceInitPriority getInitPriority() + { + return RsServiceInitPriority.NORMAL; + } + + @Override + public void initialize(PeerConnection peerConnection) + { + peerConnection.scheduleAtFixedRate(() -> writeItem(peerConnection, new HeartbeatItem()), + 5, + 5, + TimeUnit.SECONDS); + } + + @Override + public void handleItem(PeerConnection peerConnection, Item item) + { + // do nothing + } +} diff --git a/app/src/main/java/io/xeres/app/xrs/service/heartbeat/item/HeartbeatItem.java b/app/src/main/java/io/xeres/app/xrs/service/heartbeat/item/HeartbeatItem.java new file mode 100644 index 000000000..a6db5969c --- /dev/null +++ b/app/src/main/java/io/xeres/app/xrs/service/heartbeat/item/HeartbeatItem.java @@ -0,0 +1,31 @@ +/* + * Copyright (c) 2019-2020 by David Gerber - https://zapek.com + * + * This file is part of Xeres. + * + * Xeres is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Xeres is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Xeres. If not, see . + */ + +package io.xeres.app.xrs.service.heartbeat.item; + +import io.xeres.app.xrs.item.Item; + +public class HeartbeatItem extends Item +{ + @Override + public String toString() + { + return "HeartbeatItem"; + } +} diff --git a/app/src/main/java/io/xeres/app/xrs/service/rtt/RttService.java b/app/src/main/java/io/xeres/app/xrs/service/rtt/RttService.java new file mode 100644 index 000000000..8b9768b94 --- /dev/null +++ b/app/src/main/java/io/xeres/app/xrs/service/rtt/RttService.java @@ -0,0 +1,127 @@ +/* + * Copyright (c) 2019-2021 by David Gerber - https://zapek.com + * + * This file is part of Xeres. + * + * Xeres is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Xeres is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Xeres. If not, see . + */ + +package io.xeres.app.xrs.service.rtt; + +import io.xeres.app.net.peer.PeerConnection; +import io.xeres.app.net.peer.PeerConnectionManager; +import io.xeres.app.xrs.item.Item; +import io.xeres.app.xrs.service.RsService; +import io.xeres.app.xrs.service.RsServiceInitPriority; +import io.xeres.app.xrs.service.RsServiceType; +import io.xeres.app.xrs.service.rtt.item.RttPingItem; +import io.xeres.app.xrs.service.rtt.item.RttPongItem; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.core.env.Environment; +import org.springframework.stereotype.Component; + +import java.time.Duration; +import java.time.Instant; +import java.time.temporal.ChronoUnit; +import java.util.Map; +import java.util.concurrent.TimeUnit; + +import static io.xeres.app.xrs.service.RsServiceType.RTT; + +@Component +public class RttService extends RsService +{ + private static final Logger log = LoggerFactory.getLogger(RttService.class); + + private static final int KEY_COUNTER = 1; + + public RttService(Environment environment, PeerConnectionManager peerConnectionManager) + { + super(environment, peerConnectionManager); + } + + @Override + public RsServiceType getServiceType() + { + return RTT; + } + + public Map, Integer> getSupportedItems() + { + return Map.of( + RttPingItem.class, 1, + RttPongItem.class, 2 + ); + } + + @Override + public RsServiceInitPriority getInitPriority() + { + return RsServiceInitPriority.NORMAL; + } + + @Override + public void initialize(PeerConnection peerConnection) + { + peerConnection.scheduleAtFixedRate( + () -> writeItem(peerConnection, new RttPingItem(getCounter(peerConnection), get64bitsTimeStamp())), + 0, + 10, + TimeUnit.SECONDS); + } + + private int getCounter(PeerConnection peerConnection) + { + int counter = (int) peerConnection.getServiceData(this, KEY_COUNTER).orElse(1); + peerConnection.putServiceData(this, KEY_COUNTER, ++counter); + return counter; + } + + private static long get64bitsTimeStamp() + { + Instant now = Instant.now().truncatedTo(ChronoUnit.MICROS); + + return (now.getEpochSecond() << 32) + now.getNano() / 1_000L; + } + + private static Instant getInstantFromTimestamp(long timestamp) + { + return Instant.ofEpochSecond(timestamp >> 32 & 0xffffffffL, (timestamp & 0xffffffffL) * 1_000L); + } + + @Override + public void handleItem(PeerConnection peerConnection, Item item) + { + if (item instanceof RttPingItem pingItem) + { + var pong = new RttPongItem(pingItem, get64bitsTimeStamp()); + writeItem(peerConnection, pong); + } + else if (item instanceof RttPongItem pongItem) + { + var now = Instant.now(); + var ping = getInstantFromTimestamp(pongItem.getPingTimestamp()); + var pong = getInstantFromTimestamp(pongItem.getPongTimestamp()); + + var rtt = Duration.between(ping, now); + var offset = Duration.between(pong, now.minus(rtt.dividedBy(2))); + Instant peerTime = now.plus(offset); + + log.debug("RTT: {}, offset: {}", rtt, offset); + + // To calculate a mean time offset, keep ~20 of them and compute the average between them. XXX: see how RS does it and store it in the peerConnection has an extra value + } + } +} diff --git a/app/src/main/java/io/xeres/app/xrs/service/rtt/item/RttPingItem.java b/app/src/main/java/io/xeres/app/xrs/service/rtt/item/RttPingItem.java new file mode 100644 index 000000000..9ddd7e3e3 --- /dev/null +++ b/app/src/main/java/io/xeres/app/xrs/service/rtt/item/RttPingItem.java @@ -0,0 +1,62 @@ +/* + * Copyright (c) 2019-2020 by David Gerber - https://zapek.com + * + * This file is part of Xeres. + * + * Xeres is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Xeres is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Xeres. If not, see . + */ + +package io.xeres.app.xrs.service.rtt.item; + +import io.xeres.app.xrs.item.Item; +import io.xeres.app.xrs.serialization.RsSerialized; + +public class RttPingItem extends Item +{ + @RsSerialized + private int sequenceNumber; + + @RsSerialized + private long timestamp; + + public RttPingItem() + { + // Needed + } + + public RttPingItem(int sequenceNumber, long timeStamp) + { + this.sequenceNumber = sequenceNumber; + timestamp = timeStamp; + } + + public int getSequenceNumber() + { + return sequenceNumber; + } + + public long getTimestamp() + { + return timestamp; + } + + @Override + public String toString() + { + return "RttPingItem{" + + "sequenceNumber=" + sequenceNumber + + ", pingTimeStamp=" + timestamp + + '}'; + } +} diff --git a/app/src/main/java/io/xeres/app/xrs/service/rtt/item/RttPongItem.java b/app/src/main/java/io/xeres/app/xrs/service/rtt/item/RttPongItem.java new file mode 100644 index 000000000..18fc998cd --- /dev/null +++ b/app/src/main/java/io/xeres/app/xrs/service/rtt/item/RttPongItem.java @@ -0,0 +1,66 @@ +/* + * Copyright (c) 2019-2020 by David Gerber - https://zapek.com + * + * This file is part of Xeres. + * + * Xeres is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Xeres is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Xeres. If not, see . + */ + +package io.xeres.app.xrs.service.rtt.item; + +import io.xeres.app.xrs.item.Item; +import io.xeres.app.xrs.serialization.RsSerialized; + +public class RttPongItem extends Item +{ + @RsSerialized + private int sequenceNumber; + + @RsSerialized + private long pingTimestamp; + + @RsSerialized + private long pongTimestamp; + + public RttPongItem() + { + } + + public RttPongItem(RttPingItem pingItem, long timeStamp) + { + sequenceNumber = pingItem.getSequenceNumber(); + pingTimestamp = pingItem.getTimestamp(); + pongTimestamp = timeStamp; + } + + public long getPingTimestamp() + { + return pingTimestamp; + } + + public long getPongTimestamp() + { + return pongTimestamp; + } + + @Override + public String toString() + { + return "RttPongItem{" + + "sequenceNumber=" + sequenceNumber + + ", pingTimeStamp=" + pingTimestamp + + ", pongTimeStamp=" + pongTimestamp + + '}'; + } +} diff --git a/app/src/main/java/io/xeres/app/xrs/service/serviceinfo/ServiceInfoService.java b/app/src/main/java/io/xeres/app/xrs/service/serviceinfo/ServiceInfoService.java new file mode 100644 index 000000000..46bc39fe0 --- /dev/null +++ b/app/src/main/java/io/xeres/app/xrs/service/serviceinfo/ServiceInfoService.java @@ -0,0 +1,137 @@ +/* + * Copyright (c) 2019-2021 by David Gerber - https://zapek.com + * + * This file is part of Xeres. + * + * Xeres is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Xeres is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Xeres. If not, see . + */ + +package io.xeres.app.xrs.service.serviceinfo; + +import io.xeres.app.net.peer.PeerConnection; +import io.xeres.app.net.peer.PeerConnectionManager; +import io.xeres.app.xrs.item.Item; +import io.xeres.app.xrs.service.RsService; +import io.xeres.app.xrs.service.RsServiceInitPriority; +import io.xeres.app.xrs.service.RsServiceRegistry; +import io.xeres.app.xrs.service.RsServiceType; +import io.xeres.app.xrs.service.serviceinfo.item.ServiceInfo; +import io.xeres.app.xrs.service.serviceinfo.item.ServiceListItem; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.core.env.Environment; +import org.springframework.stereotype.Component; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.PriorityQueue; +import java.util.concurrent.ThreadLocalRandom; +import java.util.concurrent.TimeUnit; +import java.util.function.Predicate; + +import static io.xeres.app.xrs.service.RsServiceType.PACKET_SLICING_PROBE; +import static io.xeres.app.xrs.service.RsServiceType.SERVICEINFO; +import static java.util.stream.Collectors.joining; + +@Component +public class ServiceInfoService extends RsService +{ + private static final Logger log = LoggerFactory.getLogger(ServiceInfoService.class); + + public ServiceInfoService(Environment environment, PeerConnectionManager peerConnectionManager) + { + super(environment, peerConnectionManager); + } + + public void init(PeerConnection peerConnection) + { + sendFirstServiceList(peerConnection); //XXX: if sending and receiving at the same time (5 seconds makes it happen), then it can resend back. solution? put a timer before sending back? + } + + @Override + public RsServiceType getServiceType() + { + return SERVICEINFO; + } + + @Override + public Map, Integer> getSupportedItems() + { + return Map.of(ServiceListItem.class, 1); + } + + @Override + public void handleItem(PeerConnection peerConnection, Item item) + { + if (item instanceof ServiceListItem serviceListItem) + { + PriorityQueue services = new PriorityQueue<>(); + + serviceListItem.getServices().forEach((integer, serviceInfo) -> + { + var rsService = RsServiceRegistry.getServiceFromType(serviceInfo.getType()); + if (rsService != null) + { + peerConnection.addService(rsService); + services.add(rsService); + } + }); + if (log.isDebugEnabled()) + { + log.debug("Enabling services {} to peer {}", services.stream().map(rsService -> rsService.getServiceType().name()).collect(joining(", ")), peerConnection); + } + sendFirstServiceList(peerConnection); + + initializeServices(peerConnection, services); + } + } + + private void sendFirstServiceList(PeerConnection peerConnection) + { + if (!peerConnection.hasSentServices()) + { + HashMap services = new HashMap<>(); + + List allServices = RsServiceRegistry.getServices(); + allServices.stream() + .filter(Predicate.not(rsService -> rsService.getServiceType() == PACKET_SLICING_PROBE)) // we hide this as it's not strictly a service in RS' terms + .forEach(rsService -> + { + RsServiceType serviceType = rsService.getServiceType(); + int type = 2 << 24 | rsService.getServiceType().getType() << 8; + services.put(type, new ServiceInfo(serviceType.getName(), type, rsService.getServiceType().getVersionMajor(), rsService.getServiceType().getVersionMinor())); + }); + + writeItem(peerConnection, new ServiceListItem(services)); + peerConnection.setServicesSent(); + } + } + + private void initializeServices(PeerConnection peerConnection, PriorityQueue services) + { + RsService rsService; + + while ((rsService = services.poll()) != null) + { + if (rsService.getInitPriority() != RsServiceInitPriority.OFF) + { + var finalRsService = rsService; + peerConnection.schedule(() -> finalRsService.initialize(peerConnection), + ThreadLocalRandom.current().nextInt(rsService.getInitPriority().getMinTime(), rsService.getInitPriority().getMaxTime() + 1), + TimeUnit.SECONDS); + } + } + } +} diff --git a/app/src/main/java/io/xeres/app/xrs/service/serviceinfo/item/ServiceInfo.java b/app/src/main/java/io/xeres/app/xrs/service/serviceinfo/item/ServiceInfo.java new file mode 100644 index 000000000..fa602af3f --- /dev/null +++ b/app/src/main/java/io/xeres/app/xrs/service/serviceinfo/item/ServiceInfo.java @@ -0,0 +1,123 @@ +/* + * Copyright (c) 2019-2021 by David Gerber - https://zapek.com + * + * This file is part of Xeres. + * + * Xeres is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Xeres is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Xeres. If not, see . + */ + +package io.xeres.app.xrs.service.serviceinfo.item; + +import io.netty.buffer.ByteBuf; +import io.xeres.app.xrs.serialization.RsSerializable; +import io.xeres.app.xrs.serialization.SerializationFlags; + +import java.util.Set; + +import static io.xeres.app.xrs.serialization.Serializer.*; + +public class ServiceInfo implements RsSerializable +{ + private String name; + private int serviceType; + private short versionMajor; + private short versionMinor; + private short minVersionMajor; + private short minVersionMinor; + + public ServiceInfo() + { + } + + public ServiceInfo(String name, int serviceType, short versionMajor, short versionMinor) + { + this.name = name; + this.serviceType = serviceType; + this.versionMajor = versionMajor; + this.versionMinor = versionMinor; + minVersionMajor = versionMajor; + minVersionMinor = versionMinor; + } + + @Override + public int writeObject(ByteBuf buf, Set serializationFlags) + { + var size = 0; + + size += serialize(buf, name); + size += serialize(buf, serviceType); + size += serialize(buf, versionMajor); + size += serialize(buf, versionMinor); + size += serialize(buf, minVersionMajor); + size += serialize(buf, minVersionMinor); + return size; + } + + @Override + public void readObject(ByteBuf buf, Set serializationFlags) + { + name = deserializeString(buf); + serviceType = deserializeInt(buf); + versionMajor = deserializeShort(buf); + versionMinor = deserializeShort(buf); + minVersionMajor = deserializeShort(buf); + minVersionMinor = deserializeShort(buf); + } + + public String getName() + { + return name; + } + + public int getServiceType() + { + return serviceType; + } + + public int getType() + { + return (serviceType >> 8) & 0xffff; + } + + public short getVersionMajor() + { + return versionMajor; + } + + public short getVersionMinor() + { + return versionMinor; + } + + public short getMinVersionMajor() + { + return minVersionMajor; + } + + public short getMinVersionMinor() + { + return minVersionMinor; + } + + @Override + public String toString() + { + return "ServiceInfo{" + + "name='" + name + '\'' + + ", type=" + serviceType + + ", version=" + versionMajor + "." + versionMinor + + ", min=" + minVersionMajor + "." + minVersionMinor + + '}'; + } +} diff --git a/app/src/main/java/io/xeres/app/xrs/service/serviceinfo/item/ServiceListItem.java b/app/src/main/java/io/xeres/app/xrs/service/serviceinfo/item/ServiceListItem.java new file mode 100644 index 000000000..cef80861b --- /dev/null +++ b/app/src/main/java/io/xeres/app/xrs/service/serviceinfo/item/ServiceListItem.java @@ -0,0 +1,61 @@ +/* + * Copyright (c) 2019-2021 by David Gerber - https://zapek.com + * + * This file is part of Xeres. + * + * Xeres is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Xeres is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Xeres. If not, see . + */ + +package io.xeres.app.xrs.service.serviceinfo.item; + +import io.xeres.app.xrs.item.Item; +import io.xeres.app.xrs.serialization.RsSerialized; + +import java.util.HashMap; +import java.util.Map; + +public class ServiceListItem extends Item +{ + @RsSerialized + private Map services = new HashMap<>(); + + public ServiceListItem() + { + // Constructor + } + + public ServiceListItem(Map services) + { + this.services = services; + } + + public Map getServices() + { + return services; + } + + @Override + public int getPriority() + { + return 7; + } + + @Override + public String toString() + { + return "ServiceListItem{" + + "map=" + services.values() + + '}'; + } +} diff --git a/app/src/main/java/io/xeres/app/xrs/service/sliceprobe/SliceProbeService.java b/app/src/main/java/io/xeres/app/xrs/service/sliceprobe/SliceProbeService.java new file mode 100644 index 000000000..0a221dc1f --- /dev/null +++ b/app/src/main/java/io/xeres/app/xrs/service/sliceprobe/SliceProbeService.java @@ -0,0 +1,71 @@ +/* + * Copyright (c) 2019-2020 by David Gerber - https://zapek.com + * + * This file is part of Xeres. + * + * Xeres is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Xeres is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Xeres. If not, see . + */ + +package io.xeres.app.xrs.service.sliceprobe; + +import io.xeres.app.net.peer.PeerAttribute; +import io.xeres.app.net.peer.PeerConnection; +import io.xeres.app.net.peer.PeerConnectionManager; +import io.xeres.app.xrs.item.Item; +import io.xeres.app.xrs.service.RsService; +import io.xeres.app.xrs.service.RsServiceType; +import io.xeres.app.xrs.service.sliceprobe.item.SliceProbeItem; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.core.env.Environment; +import org.springframework.stereotype.Component; + +import java.util.Map; + +import static io.xeres.app.xrs.service.RsServiceType.PACKET_SLICING_PROBE; + +@Component +public class SliceProbeService extends RsService +{ + private static final Logger log = LoggerFactory.getLogger(SliceProbeService.class); + + public SliceProbeService(Environment environment, PeerConnectionManager peerConnectionManager) + { + super(environment, peerConnectionManager); + } + + @Override + public RsServiceType getServiceType() + { + return PACKET_SLICING_PROBE; + } + + @Override + public Map, Integer> getSupportedItems() + { + return Map.of( + SliceProbeItem.class, 0xCC + ); + } + + @Override + public void handleItem(PeerConnection peerConnection, Item item) + { + if (!Boolean.TRUE.equals(peerConnection.getCtx().channel().attr(PeerAttribute.MULTI_PACKET).get())) + { + log.debug("Received slice probe, switching to new packet format for current session"); + peerConnection.getCtx().channel().attr(PeerAttribute.MULTI_PACKET).set(true); + } + } +} diff --git a/app/src/main/java/io/xeres/app/xrs/service/sliceprobe/item/SliceProbeItem.java b/app/src/main/java/io/xeres/app/xrs/service/sliceprobe/item/SliceProbeItem.java new file mode 100644 index 000000000..46d8b9ff4 --- /dev/null +++ b/app/src/main/java/io/xeres/app/xrs/service/sliceprobe/item/SliceProbeItem.java @@ -0,0 +1,31 @@ +/* + * Copyright (c) 2019-2020 by David Gerber - https://zapek.com + * + * This file is part of Xeres. + * + * Xeres is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Xeres is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Xeres. If not, see . + */ + +package io.xeres.app.xrs.service.sliceprobe.item; + +import io.xeres.app.xrs.item.Item; + +public class SliceProbeItem extends Item +{ + @Override + public String toString() + { + return "SliceProbeItem"; + } +} diff --git a/app/src/main/java/io/xeres/app/xrs/service/status/StatusService.java b/app/src/main/java/io/xeres/app/xrs/service/status/StatusService.java new file mode 100644 index 000000000..2ded1ce2c --- /dev/null +++ b/app/src/main/java/io/xeres/app/xrs/service/status/StatusService.java @@ -0,0 +1,86 @@ +/* + * Copyright (c) 2019-2021 by David Gerber - https://zapek.com + * + * This file is part of Xeres. + * + * Xeres is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Xeres is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Xeres. If not, see . + */ + +package io.xeres.app.xrs.service.status; + +import io.xeres.app.net.peer.PeerConnection; +import io.xeres.app.net.peer.PeerConnectionManager; +import io.xeres.app.xrs.item.Item; +import io.xeres.app.xrs.service.RsService; +import io.xeres.app.xrs.service.RsServiceInitPriority; +import io.xeres.app.xrs.service.RsServiceType; +import io.xeres.app.xrs.service.status.item.StatusItem; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.core.env.Environment; +import org.springframework.stereotype.Component; + +import java.util.Map; +import java.util.concurrent.TimeUnit; + +import static io.xeres.app.xrs.service.RsServiceType.STATUS; +import static io.xeres.app.xrs.service.status.item.StatusItem.Status.ONLINE; + +@Component +public class StatusService extends RsService +{ + private static final Logger log = LoggerFactory.getLogger(StatusService.class); + + public StatusService(Environment environment, PeerConnectionManager peerConnectionManager) + { + super(environment, peerConnectionManager); + } + + @Override + public RsServiceType getServiceType() + { + return STATUS; + } + + @Override + public Map, Integer> getSupportedItems() + { + return Map.of(StatusItem.class, 1); + } + + @Override + public RsServiceInitPriority getInitPriority() + { + return RsServiceInitPriority.NORMAL; + } + + @Override + public void initialize(PeerConnection peerConnection) + { + peerConnection.schedule( + () -> writeItem(peerConnection, new StatusItem(ONLINE)), + 0, + TimeUnit.SECONDS); + } + + @Override + public void handleItem(PeerConnection peerConnection, Item item) + { + // XXX: print peer's status (ideally refresh a list) + if (item instanceof StatusItem statusItem) + { + log.debug("Got status {} from peer {}", statusItem.getStatus(), peerConnection); + } + } +} diff --git a/app/src/main/java/io/xeres/app/xrs/service/status/item/StatusItem.java b/app/src/main/java/io/xeres/app/xrs/service/status/item/StatusItem.java new file mode 100644 index 000000000..f1cf43a46 --- /dev/null +++ b/app/src/main/java/io/xeres/app/xrs/service/status/item/StatusItem.java @@ -0,0 +1,75 @@ +/* + * Copyright (c) 2019-2021 by David Gerber - https://zapek.com + * + * This file is part of Xeres. + * + * Xeres is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Xeres is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Xeres. If not, see . + */ + +package io.xeres.app.xrs.service.status.item; + +import io.xeres.app.xrs.item.Item; +import io.xeres.app.xrs.serialization.RsSerialized; + +import java.time.Instant; + +public class StatusItem extends Item +{ + public enum Status + { + OFFLINE, + AWAY, + BUSY, + ONLINE, + INACTIVE + } + + @RsSerialized + private int sendTime; + + @RsSerialized + private Status status; + + + @SuppressWarnings("unused") + public StatusItem() + { + // Required + } + + public StatusItem(Status status) + { + this.sendTime = (int) Instant.now().getEpochSecond(); + this.status = status; + } + + public int getSendTime() + { + return sendTime; + } + + public Enum getStatus() + { + return status; + } + + @Override + public String toString() + { + return "StatusItem{" + + "sendTime=" + sendTime + + ", status=" + status + + '}'; + } +} diff --git a/app/src/main/resources/LICENSE b/app/src/main/resources/LICENSE new file mode 100644 index 000000000..20d40b6bc --- /dev/null +++ b/app/src/main/resources/LICENSE @@ -0,0 +1,674 @@ + GNU GENERAL PUBLIC LICENSE + Version 3, 29 June 2007 + + Copyright (C) 2007 Free Software Foundation, Inc. + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + Preamble + + The GNU General Public License is a free, copyleft license for +software and other kinds of works. + + The licenses for most software and other practical works are designed +to take away your freedom to share and change the works. By contrast, +the GNU General Public License is intended to guarantee your freedom to +share and change all versions of a program--to make sure it remains free +software for all its users. We, the Free Software Foundation, use the +GNU General Public License for most of our software; it applies also to +any other work released this way by its authors. You can apply it to +your programs, too. + + When we speak of free software, we are referring to freedom, not +price. Our General Public Licenses are designed to make sure that you +have the freedom to distribute copies of free software (and charge for +them if you wish), that you receive source code or can get it if you +want it, that you can change the software or use pieces of it in new +free programs, and that you know you can do these things. + + To protect your rights, we need to prevent others from denying you +these rights or asking you to surrender the rights. Therefore, you have +certain responsibilities if you distribute copies of the software, or if +you modify it: responsibilities to respect the freedom of others. + + For example, if you distribute copies of such a program, whether +gratis or for a fee, you must pass on to the recipients the same +freedoms that you received. You must make sure that they, too, receive +or can get the source code. And you must show them these terms so they +know their rights. + + Developers that use the GNU GPL protect your rights with two steps: +(1) assert copyright on the software, and (2) offer you this License +giving you legal permission to copy, distribute and/or modify it. + + For the developers' and authors' protection, the GPL clearly explains +that there is no warranty for this free software. For both users' and +authors' sake, the GPL requires that modified versions be marked as +changed, so that their problems will not be attributed erroneously to +authors of previous versions. + + Some devices are designed to deny users access to install or run +modified versions of the software inside them, although the manufacturer +can do so. This is fundamentally incompatible with the aim of +protecting users' freedom to change the software. The systematic +pattern of such abuse occurs in the area of products for individuals to +use, which is precisely where it is most unacceptable. Therefore, we +have designed this version of the GPL to prohibit the practice for those +products. If such problems arise substantially in other domains, we +stand ready to extend this provision to those domains in future versions +of the GPL, as needed to protect the freedom of users. + + Finally, every program is threatened constantly by software patents. +States should not allow patents to restrict development and use of +software on general-purpose computers, but in those that do, we wish to +avoid the special danger that patents applied to a free program could +make it effectively proprietary. To prevent this, the GPL assures that +patents cannot be used to render the program non-free. + + The precise terms and conditions for copying, distribution and +modification follow. + + TERMS AND CONDITIONS + + 0. Definitions. + + "This License" refers to version 3 of the GNU General Public License. + + "Copyright" also means copyright-like laws that apply to other kinds of +works, such as semiconductor masks. + + "The Program" refers to any copyrightable work licensed under this +License. Each licensee is addressed as "you". "Licensees" and +"recipients" may be individuals or organizations. + + To "modify" a work means to copy from or adapt all or part of the work +in a fashion requiring copyright permission, other than the making of an +exact copy. The resulting work is called a "modified version" of the +earlier work or a work "based on" the earlier work. + + A "covered work" means either the unmodified Program or a work based +on the Program. + + To "propagate" a work means to do anything with it that, without +permission, would make you directly or secondarily liable for +infringement under applicable copyright law, except executing it on a +computer or modifying a private copy. Propagation includes copying, +distribution (with or without modification), making available to the +public, and in some countries other activities as well. + + To "convey" a work means any kind of propagation that enables other +parties to make or receive copies. Mere interaction with a user through +a computer network, with no transfer of a copy, is not conveying. + + An interactive user interface displays "Appropriate Legal Notices" +to the extent that it includes a convenient and prominently visible +feature that (1) displays an appropriate copyright notice, and (2) +tells the user that there is no warranty for the work (except to the +extent that warranties are provided), that licensees may convey the +work under this License, and how to view a copy of this License. If +the interface presents a list of user commands or options, such as a +menu, a prominent item in the list meets this criterion. + + 1. Source Code. + + The "source code" for a work means the preferred form of the work +for making modifications to it. "Object code" means any non-source +form of a work. + + A "Standard Interface" means an interface that either is an official +standard defined by a recognized standards body, or, in the case of +interfaces specified for a particular programming language, one that +is widely used among developers working in that language. + + The "System Libraries" of an executable work include anything, other +than the work as a whole, that (a) is included in the normal form of +packaging a Major Component, but which is not part of that Major +Component, and (b) serves only to enable use of the work with that +Major Component, or to implement a Standard Interface for which an +implementation is available to the public in source code form. A +"Major Component", in this context, means a major essential component +(kernel, window system, and so on) of the specific operating system +(if any) on which the executable work runs, or a compiler used to +produce the work, or an object code interpreter used to run it. + + The "Corresponding Source" for a work in object code form means all +the source code needed to generate, install, and (for an executable +work) run the object code and to modify the work, including scripts to +control those activities. However, it does not include the work's +System Libraries, or general-purpose tools or generally available free +programs which are used unmodified in performing those activities but +which are not part of the work. For example, Corresponding Source +includes interface definition files associated with source files for +the work, and the source code for shared libraries and dynamically +linked subprograms that the work is specifically designed to require, +such as by intimate data communication or control flow between those +subprograms and other parts of the work. + + The Corresponding Source need not include anything that users +can regenerate automatically from other parts of the Corresponding +Source. + + The Corresponding Source for a work in source code form is that +same work. + + 2. Basic Permissions. + + All rights granted under this License are granted for the term of +copyright on the Program, and are irrevocable provided the stated +conditions are met. This License explicitly affirms your unlimited +permission to run the unmodified Program. The output from running a +covered work is covered by this License only if the output, given its +content, constitutes a covered work. This License acknowledges your +rights of fair use or other equivalent, as provided by copyright law. + + You may make, run and propagate covered works that you do not +convey, without conditions so long as your license otherwise remains +in force. You may convey covered works to others for the sole purpose +of having them make modifications exclusively for you, or provide you +with facilities for running those works, provided that you comply with +the terms of this License in conveying all material for which you do +not control copyright. Those thus making or running the covered works +for you must do so exclusively on your behalf, under your direction +and control, on terms that prohibit them from making any copies of +your copyrighted material outside their relationship with you. + + Conveying under any other circumstances is permitted solely under +the conditions stated below. Sublicensing is not allowed; section 10 +makes it unnecessary. + + 3. Protecting Users' Legal Rights From Anti-Circumvention Law. + + No covered work shall be deemed part of an effective technological +measure under any applicable law fulfilling obligations under article +11 of the WIPO copyright treaty adopted on 20 December 1996, or +similar laws prohibiting or restricting circumvention of such +measures. + + When you convey a covered work, you waive any legal power to forbid +circumvention of technological measures to the extent such circumvention +is effected by exercising rights under this License with respect to +the covered work, and you disclaim any intention to limit operation or +modification of the work as a means of enforcing, against the work's +users, your or third parties' legal rights to forbid circumvention of +technological measures. + + 4. Conveying Verbatim Copies. + + You may convey verbatim copies of the Program's source code as you +receive it, in any medium, provided that you conspicuously and +appropriately publish on each copy an appropriate copyright notice; +keep intact all notices stating that this License and any +non-permissive terms added in accord with section 7 apply to the code; +keep intact all notices of the absence of any warranty; and give all +recipients a copy of this License along with the Program. + + You may charge any price or no price for each copy that you convey, +and you may offer support or warranty protection for a fee. + + 5. Conveying Modified Source Versions. + + You may convey a work based on the Program, or the modifications to +produce it from the Program, in the form of source code under the +terms of section 4, provided that you also meet all of these conditions: + + a) The work must carry prominent notices stating that you modified + it, and giving a relevant date. + + b) The work must carry prominent notices stating that it is + released under this License and any conditions added under section + 7. This requirement modifies the requirement in section 4 to + "keep intact all notices". + + c) You must license the entire work, as a whole, under this + License to anyone who comes into possession of a copy. This + License will therefore apply, along with any applicable section 7 + additional terms, to the whole of the work, and all its parts, + regardless of how they are packaged. This License gives no + permission to license the work in any other way, but it does not + invalidate such permission if you have separately received it. + + d) If the work has interactive user interfaces, each must display + Appropriate Legal Notices; however, if the Program has interactive + interfaces that do not display Appropriate Legal Notices, your + work need not make them do so. + + A compilation of a covered work with other separate and independent +works, which are not by their nature extensions of the covered work, +and which are not combined with it such as to form a larger program, +in or on a volume of a storage or distribution medium, is called an +"aggregate" if the compilation and its resulting copyright are not +used to limit the access or legal rights of the compilation's users +beyond what the individual works permit. Inclusion of a covered work +in an aggregate does not cause this License to apply to the other +parts of the aggregate. + + 6. Conveying Non-Source Forms. + + You may convey a covered work in object code form under the terms +of sections 4 and 5, provided that you also convey the +machine-readable Corresponding Source under the terms of this License, +in one of these ways: + + a) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by the + Corresponding Source fixed on a durable physical medium + customarily used for software interchange. + + b) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by a + written offer, valid for at least three years and valid for as + long as you offer spare parts or customer support for that product + model, to give anyone who possesses the object code either (1) a + copy of the Corresponding Source for all the software in the + product that is covered by this License, on a durable physical + medium customarily used for software interchange, for a price no + more than your reasonable cost of physically performing this + conveying of source, or (2) access to copy the + Corresponding Source from a network server at no charge. + + c) Convey individual copies of the object code with a copy of the + written offer to provide the Corresponding Source. This + alternative is allowed only occasionally and noncommercially, and + only if you received the object code with such an offer, in accord + with subsection 6b. + + d) Convey the object code by offering access from a designated + place (gratis or for a charge), and offer equivalent access to the + Corresponding Source in the same way through the same place at no + further charge. You need not require recipients to copy the + Corresponding Source along with the object code. If the place to + copy the object code is a network server, the Corresponding Source + may be on a different server (operated by you or a third party) + that supports equivalent copying facilities, provided you maintain + clear directions next to the object code saying where to find the + Corresponding Source. Regardless of what server hosts the + Corresponding Source, you remain obligated to ensure that it is + available for as long as needed to satisfy these requirements. + + e) Convey the object code using peer-to-peer transmission, provided + you inform other peers where the object code and Corresponding + Source of the work are being offered to the general public at no + charge under subsection 6d. + + A separable portion of the object code, whose source code is excluded +from the Corresponding Source as a System Library, need not be +included in conveying the object code work. + + A "User Product" is either (1) a "consumer product", which means any +tangible personal property which is normally used for personal, family, +or household purposes, or (2) anything designed or sold for incorporation +into a dwelling. In determining whether a product is a consumer product, +doubtful cases shall be resolved in favor of coverage. For a particular +product received by a particular user, "normally used" refers to a +typical or common use of that class of product, regardless of the status +of the particular user or of the way in which the particular user +actually uses, or expects or is expected to use, the product. A product +is a consumer product regardless of whether the product has substantial +commercial, industrial or non-consumer uses, unless such uses represent +the only significant mode of use of the product. + + "Installation Information" for a User Product means any methods, +procedures, authorization keys, or other information required to install +and execute modified versions of a covered work in that User Product from +a modified version of its Corresponding Source. The information must +suffice to ensure that the continued functioning of the modified object +code is in no case prevented or interfered with solely because +modification has been made. + + If you convey an object code work under this section in, or with, or +specifically for use in, a User Product, and the conveying occurs as +part of a transaction in which the right of possession and use of the +User Product is transferred to the recipient in perpetuity or for a +fixed term (regardless of how the transaction is characterized), the +Corresponding Source conveyed under this section must be accompanied +by the Installation Information. But this requirement does not apply +if neither you nor any third party retains the ability to install +modified object code on the User Product (for example, the work has +been installed in ROM). + + The requirement to provide Installation Information does not include a +requirement to continue to provide support service, warranty, or updates +for a work that has been modified or installed by the recipient, or for +the User Product in which it has been modified or installed. Access to a +network may be denied when the modification itself materially and +adversely affects the operation of the network or violates the rules and +protocols for communication across the network. + + Corresponding Source conveyed, and Installation Information provided, +in accord with this section must be in a format that is publicly +documented (and with an implementation available to the public in +source code form), and must require no special password or key for +unpacking, reading or copying. + + 7. Additional Terms. + + "Additional permissions" are terms that supplement the terms of this +License by making exceptions from one or more of its conditions. +Additional permissions that are applicable to the entire Program shall +be treated as though they were included in this License, to the extent +that they are valid under applicable law. If additional permissions +apply only to part of the Program, that part may be used separately +under those permissions, but the entire Program remains governed by +this License without regard to the additional permissions. + + When you convey a copy of a covered work, you may at your option +remove any additional permissions from that copy, or from any part of +it. (Additional permissions may be written to require their own +removal in certain cases when you modify the work.) You may place +additional permissions on material, added by you to a covered work, +for which you have or can give appropriate copyright permission. + + Notwithstanding any other provision of this License, for material you +add to a covered work, you may (if authorized by the copyright holders of +that material) supplement the terms of this License with terms: + + a) Disclaiming warranty or limiting liability differently from the + terms of sections 15 and 16 of this License; or + + b) Requiring preservation of specified reasonable legal notices or + author attributions in that material or in the Appropriate Legal + Notices displayed by works containing it; or + + c) Prohibiting misrepresentation of the origin of that material, or + requiring that modified versions of such material be marked in + reasonable ways as different from the original version; or + + d) Limiting the use for publicity purposes of names of licensors or + authors of the material; or + + e) Declining to grant rights under trademark law for use of some + trade names, trademarks, or service marks; or + + f) Requiring indemnification of licensors and authors of that + material by anyone who conveys the material (or modified versions of + it) with contractual assumptions of liability to the recipient, for + any liability that these contractual assumptions directly impose on + those licensors and authors. + + All other non-permissive additional terms are considered "further +restrictions" within the meaning of section 10. If the Program as you +received it, or any part of it, contains a notice stating that it is +governed by this License along with a term that is a further +restriction, you may remove that term. If a license document contains +a further restriction but permits relicensing or conveying under this +License, you may add to a covered work material governed by the terms +of that license document, provided that the further restriction does +not survive such relicensing or conveying. + + If you add terms to a covered work in accord with this section, you +must place, in the relevant source files, a statement of the +additional terms that apply to those files, or a notice indicating +where to find the applicable terms. + + Additional terms, permissive or non-permissive, may be stated in the +form of a separately written license, or stated as exceptions; +the above requirements apply either way. + + 8. Termination. + + You may not propagate or modify a covered work except as expressly +provided under this License. Any attempt otherwise to propagate or +modify it is void, and will automatically terminate your rights under +this License (including any patent licenses granted under the third +paragraph of section 11). + + However, if you cease all violation of this License, then your +license from a particular copyright holder is reinstated (a) +provisionally, unless and until the copyright holder explicitly and +finally terminates your license, and (b) permanently, if the copyright +holder fails to notify you of the violation by some reasonable means +prior to 60 days after the cessation. + + Moreover, your license from a particular copyright holder is +reinstated permanently if the copyright holder notifies you of the +violation by some reasonable means, this is the first time you have +received notice of violation of this License (for any work) from that +copyright holder, and you cure the violation prior to 30 days after +your receipt of the notice. + + Termination of your rights under this section does not terminate the +licenses of parties who have received copies or rights from you under +this License. If your rights have been terminated and not permanently +reinstated, you do not qualify to receive new licenses for the same +material under section 10. + + 9. Acceptance Not Required for Having Copies. + + You are not required to accept this License in order to receive or +run a copy of the Program. Ancillary propagation of a covered work +occurring solely as a consequence of using peer-to-peer transmission +to receive a copy likewise does not require acceptance. However, +nothing other than this License grants you permission to propagate or +modify any covered work. These actions infringe copyright if you do +not accept this License. Therefore, by modifying or propagating a +covered work, you indicate your acceptance of this License to do so. + + 10. Automatic Licensing of Downstream Recipients. + + Each time you convey a covered work, the recipient automatically +receives a license from the original licensors, to run, modify and +propagate that work, subject to this License. You are not responsible +for enforcing compliance by third parties with this License. + + An "entity transaction" is a transaction transferring control of an +organization, or substantially all assets of one, or subdividing an +organization, or merging organizations. If propagation of a covered +work results from an entity transaction, each party to that +transaction who receives a copy of the work also receives whatever +licenses to the work the party's predecessor in interest had or could +give under the previous paragraph, plus a right to possession of the +Corresponding Source of the work from the predecessor in interest, if +the predecessor has it or can get it with reasonable efforts. + + You may not impose any further restrictions on the exercise of the +rights granted or affirmed under this License. For example, you may +not impose a license fee, royalty, or other charge for exercise of +rights granted under this License, and you may not initiate litigation +(including a cross-claim or counterclaim in a lawsuit) alleging that +any patent claim is infringed by making, using, selling, offering for +sale, or importing the Program or any portion of it. + + 11. Patents. + + A "contributor" is a copyright holder who authorizes use under this +License of the Program or a work on which the Program is based. The +work thus licensed is called the contributor's "contributor version". + + A contributor's "essential patent claims" are all patent claims +owned or controlled by the contributor, whether already acquired or +hereafter acquired, that would be infringed by some manner, permitted +by this License, of making, using, or selling its contributor version, +but do not include claims that would be infringed only as a +consequence of further modification of the contributor version. For +purposes of this definition, "control" includes the right to grant +patent sublicenses in a manner consistent with the requirements of +this License. + + Each contributor grants you a non-exclusive, worldwide, royalty-free +patent license under the contributor's essential patent claims, to +make, use, sell, offer for sale, import and otherwise run, modify and +propagate the contents of its contributor version. + + In the following three paragraphs, a "patent license" is any express +agreement or commitment, however denominated, not to enforce a patent +(such as an express permission to practice a patent or covenant not to +sue for patent infringement). To "grant" such a patent license to a +party means to make such an agreement or commitment not to enforce a +patent against the party. + + If you convey a covered work, knowingly relying on a patent license, +and the Corresponding Source of the work is not available for anyone +to copy, free of charge and under the terms of this License, through a +publicly available network server or other readily accessible means, +then you must either (1) cause the Corresponding Source to be so +available, or (2) arrange to deprive yourself of the benefit of the +patent license for this particular work, or (3) arrange, in a manner +consistent with the requirements of this License, to extend the patent +license to downstream recipients. "Knowingly relying" means you have +actual knowledge that, but for the patent license, your conveying the +covered work in a country, or your recipient's use of the covered work +in a country, would infringe one or more identifiable patents in that +country that you have reason to believe are valid. + + If, pursuant to or in connection with a single transaction or +arrangement, you convey, or propagate by procuring conveyance of, a +covered work, and grant a patent license to some of the parties +receiving the covered work authorizing them to use, propagate, modify +or convey a specific copy of the covered work, then the patent license +you grant is automatically extended to all recipients of the covered +work and works based on it. + + A patent license is "discriminatory" if it does not include within +the scope of its coverage, prohibits the exercise of, or is +conditioned on the non-exercise of one or more of the rights that are +specifically granted under this License. You may not convey a covered +work if you are a party to an arrangement with a third party that is +in the business of distributing software, under which you make payment +to the third party based on the extent of your activity of conveying +the work, and under which the third party grants, to any of the +parties who would receive the covered work from you, a discriminatory +patent license (a) in connection with copies of the covered work +conveyed by you (or copies made from those copies), or (b) primarily +for and in connection with specific products or compilations that +contain the covered work, unless you entered into that arrangement, +or that patent license was granted, prior to 28 March 2007. + + Nothing in this License shall be construed as excluding or limiting +any implied license or other defenses to infringement that may +otherwise be available to you under applicable patent law. + + 12. No Surrender of Others' Freedom. + + If conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot convey a +covered work so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you may +not convey it at all. For example, if you agree to terms that obligate you +to collect a royalty for further conveying from those to whom you convey +the Program, the only way you could satisfy both those terms and this +License would be to refrain entirely from conveying the Program. + + 13. Use with the GNU Affero General Public License. + + Notwithstanding any other provision of this License, you have +permission to link or combine any covered work with a work licensed +under version 3 of the GNU Affero General Public License into a single +combined work, and to convey the resulting work. The terms of this +License will continue to apply to the part which is the covered work, +but the special requirements of the GNU Affero General Public License, +section 13, concerning interaction through a network will apply to the +combination as such. + + 14. Revised Versions of this License. + + The Free Software Foundation may publish revised and/or new versions of +the GNU General Public License from time to time. Such new versions will +be similar in spirit to the present version, but may differ in detail to +address new problems or concerns. + + Each version is given a distinguishing version number. If the +Program specifies that a certain numbered version of the GNU General +Public License "or any later version" applies to it, you have the +option of following the terms and conditions either of that numbered +version or of any later version published by the Free Software +Foundation. If the Program does not specify a version number of the +GNU General Public License, you may choose any version ever published +by the Free Software Foundation. + + If the Program specifies that a proxy can decide which future +versions of the GNU General Public License can be used, that proxy's +public statement of acceptance of a version permanently authorizes you +to choose that version for the Program. + + Later license versions may give you additional or different +permissions. However, no additional obligations are imposed on any +author or copyright holder as a result of your choosing to follow a +later version. + + 15. Disclaimer of Warranty. + + THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY +APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT +HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY +OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, +THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM +IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF +ALL NECESSARY SERVICING, REPAIR OR CORRECTION. + + 16. Limitation of Liability. + + IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS +THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY +GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE +USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF +DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD +PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), +EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF +SUCH DAMAGES. + + 17. Interpretation of Sections 15 and 16. + + If the disclaimer of warranty and limitation of liability provided +above cannot be given local legal effect according to their terms, +reviewing courts shall apply local law that most closely approximates +an absolute waiver of all civil liability in connection with the +Program, unless a warranty or assumption of liability accompanies a +copy of the Program in return for a fee. + + END OF TERMS AND CONDITIONS + + How to Apply These Terms to Your New Programs + + If you develop a new program, and you want it to be of the greatest +possible use to the public, the best way to achieve this is to make it +free software which everyone can redistribute and change under these terms. + + To do so, attach the following notices to the program. It is safest +to attach them to the start of each source file to most effectively +state the exclusion of warranty; and each file should have at least +the "copyright" line and a pointer to where the full notice is found. + + + Copyright (C) + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + +Also add information on how to contact you by electronic and paper mail. + + If the program does terminal interaction, make it output a short +notice like this when it starts in an interactive mode: + + Copyright (C) + This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'. + This is free software, and you are welcome to redistribute it + under certain conditions; type `show c' for details. + +The hypothetical commands `show w' and `show c' should show the appropriate +parts of the General Public License. Of course, your program's commands +might be different; for a GUI interface, you would use an "about box". + + You should also get your employer (if you work as a programmer) or school, +if any, to sign a "copyright disclaimer" for the program, if necessary. +For more information on this, and how to apply and follow the GNU GPL, see +. + + The GNU General Public License does not permit incorporating your program +into proprietary programs. If your program is a subroutine library, you +may consider it more useful to permit linking proprietary applications with +the library. If this is what you want to do, use the GNU Lesser General +Public License instead of this License. But first, please read +. \ No newline at end of file diff --git a/app/src/main/resources/META-INF/additional-spring-configuration-metadata.json b/app/src/main/resources/META-INF/additional-spring-configuration-metadata.json new file mode 100644 index 000000000..62a5cf4b1 --- /dev/null +++ b/app/src/main/resources/META-INF/additional-spring-configuration-metadata.json @@ -0,0 +1,44 @@ +{ + "properties": [ + { + "name": "xrs.service.rtt.enabled", + "type": "java.lang.Boolean", + "description": "Enable the RTT service, used to calculate Round Trip Time between peers." + }, + { + "name": "xrs.service.sliceprobe.enabled", + "type": "java.lang.Boolean", + "description": "Enable the slice probe service, used to advertise we support incoming packet slicing." + }, + { + "name": "xrs.service.serviceinfo.enabled", + "type": "java.lang.Boolean", + "description": "Enable the serviceinfo service, used to advertise which services we support." + }, + { + "name": "xrs.service.discovery.enabled", + "type": "java.lang.Boolean", + "description": "Enable the discovery service, used to exchange keys and contacts between locations." + }, + { + "name": "xrs.service.heartbeat.enabled", + "type": "java.lang.Boolean", + "description": "Enable the heartbeat service, used to ping a peer to know if it's up but kind of useless as it overlaps with sliceprobe and rtt." + }, + { + "name": "xrs.service.chat.enabled", + "type": "java.lang.Boolean", + "description": "Enable the chat service, used for distributed chat, private chat, distant chat, ..." + }, + { + "name": "xrs.service.status.enabled", + "type": "java.lang.Boolean", + "description": "Enable the status service, used to tell about status (away, busy, online, etc...)." + }, + { + "name": "xrs.service.gxsid.enabled", + "type": "java.lang.Boolean", + "description": "Enable the GxsId service, used to exchange GXS IDs." + } + ] +} \ No newline at end of file diff --git a/app/src/main/resources/application-cloud.properties b/app/src/main/resources/application-cloud.properties new file mode 100644 index 000000000..23c92bd6e --- /dev/null +++ b/app/src/main/resources/application-cloud.properties @@ -0,0 +1,20 @@ +# +# Copyright (c) 2019-2020 by David Gerber - https://zapek.com +# +# This file is part of Xeres. +# +# Xeres is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Xeres is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Xeres. If not, see . +# +# This profile is active when deployed on a cloud environment +xrs.ui.enabled=false diff --git a/app/src/main/resources/application-dev.properties b/app/src/main/resources/application-dev.properties new file mode 100644 index 000000000..488856101 --- /dev/null +++ b/app/src/main/resources/application-dev.properties @@ -0,0 +1,65 @@ +# +# Copyright (c) 2019-2021 by David Gerber - https://zapek.com +# +# This file is part of Xeres. +# +# Xeres is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Xeres is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Xeres. If not, see . +# +# Debug levels +# +# Default +logging.level.io.xeres=DEBUG +# +# Broadcast discovery debugging +logging.level.io.xeres.app.net.bdisc=INFO +# +# Serializer +#logging.level.io.xeres.app.xrs.serialization.*=TRACE +logging.level.io.xeres.app.xrs.item=INFO +#logging.level.io.xeres.app.xrs.item=TRACE +# +# Packet content +# Outgoing +logging.level.io.xeres.app.net.peer.PeerConnectionManager=INFO +#logging.level.io.xeres.app.net.peer.PeerConnectionManager=TRACE +# Incoming +logging.level.io.xeres.app.net.peer.pipeline.PeerHandler=INFO +#logging.level.io.xeres.app.net.peer.pipeline.PeerHandler=TRACE +# +# WebSocket +logging.level.org.springframework.web.socket.config.WebSocketMessageBrokerStats=WARN +# +# Services +# +# Chat +logging.level.io.xeres.app.xrs.service.chat=INFO +# Discovery +logging.level.io.xeres.app.xrs.service.discovery=INFO +# heartbeat +logging.level.io.xeres.app.xrs.service.heartbeat=INFO +# rtt +logging.level.io.xeres.app.xrs.service.rtt=INFO +# serviceinfo +logging.level.io.xeres.app.xrs.service.serviceinfo=INFO +# sliceprobe +logging.level.io.xeres.app.xrs.service.sliceprobe=INFO +# status +logging.level.io.xeres.app.xrs.service.status=INFO + +# Flyway +spring.flyway.clean-on-validation-error=true +# Actuator +management.endpoints.jmx.exposure.exclude= +# Netty +spring.netty.leak-detection=paranoid diff --git a/app/src/main/resources/application.properties b/app/src/main/resources/application.properties new file mode 100644 index 000000000..9bd826e9c --- /dev/null +++ b/app/src/main/resources/application.properties @@ -0,0 +1,52 @@ +# +# Copyright (c) 2019-2021 by David Gerber - https://zapek.com +# +# This file is part of Xeres. +# +# Xeres is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Xeres is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Xeres. If not, see . +# +spring.datasource.driver-class-name=org.h2.Driver +spring.jpa.database-platform=org.hibernate.dialect.H2Dialect +spring.jpa.open-in-view=true +# This is the default and cannot be changed here for various reasons. To set the port, use the command argument: --control-port= +server.port=1066 +xrs.ui.enabled=true +xrs.ui.address=localhost +xrs.ui.port=1066 +#xrs.ui.ssl.enabled=false XXX: add support for that +## Database +# Cache size (in KB) +xrs.db.cache-size=1024 +# Actuator +info.java.vm.vendor=${java.vm.vendor} +info.java.version=${java.version} +management.endpoints.jmx.exposure.exclude=* +management.endpoint.shutdown.enabled=true +management.endpoints.web.exposure.include=info,health,env,logfile,shutdown +management.endpoints.web.base-path=/api/v1/actuator +springdoc.show-actuator=true +## Network +# Use the new packet slicing system (currently broken) +xrs.network.packet-slicing=false +xrs.network.packet-grouping=false +xrs.network.dht=false +# RsServices +xrs.service.rtt.enabled=true +xrs.service.sliceprobe.enabled=true +xrs.service.serviceinfo.enabled=true +xrs.service.discovery.enabled=true +xrs.service.heartbeat.enabled=true +xrs.service.chat.enabled=true +xrs.service.status.enabled=true +xrs.service.gxsid.enabled=true \ No newline at end of file diff --git a/app/src/main/resources/banner.txt b/app/src/main/resources/banner.txt new file mode 100644 index 000000000..47b47e5f2 --- /dev/null +++ b/app/src/main/resources/banner.txt @@ -0,0 +1,8 @@ +__ __ +\ \ / / + \ V / ___ _ __ ___ ___ + / \ / _ \ '__/ _ \/ __| +/ /^\ \ __/ | | __/\__ \ +\/ \/\___|_| \___||___/ +:: The Ultimate Peer-to-Peer Solution :: +:: https://xeres.io :: diff --git a/app/src/main/resources/bdboot.txt b/app/src/main/resources/bdboot.txt new file mode 100644 index 000000000..5853ba519 --- /dev/null +++ b/app/src/main/resources/bdboot.txt @@ -0,0 +1,3 @@ +82.221.103.244 6881 +67.215.246.10 6881 +185.157.221.247 25401 \ No newline at end of file diff --git a/app/src/main/resources/db/migration/V00_0_1_202001232214__InitDb.sql b/app/src/main/resources/db/migration/V00_0_1_202001232214__InitDb.sql new file mode 100644 index 000000000..413668252 --- /dev/null +++ b/app/src/main/resources/db/migration/V00_0_1_202001232214__InitDb.sql @@ -0,0 +1,130 @@ +-- +-- Database creation +-- +-- Do not touch this file if unnecessary (even comments) as this will trigger +-- a flyway migration. +-- +-- See https://h2database.com/html/datatypes.html for the data types +-- +-- Do not put indexes on identifiers and fingerprints as they have a random +-- distribution that don't play well with b-trees. +-- +CREATE TABLE profiles +( + id IDENTITY NOT NULL PRIMARY KEY, + name VARCHAR(64) NOT NULL, + pgp_identifier BIGINT NOT NULL UNIQUE, + pgp_fingerprint BINARY(20) NOT NULL, + pgp_public_key_data VARBINARY(16384), + accepted BOOLEAN NOT NULL DEFAULT false, + trust ENUM ('unknown', 'never', 'marginal', 'full', 'ultimate') DEFAULT 'unknown' +); + +CREATE TABLE locations +( + id IDENTITY NOT NULL PRIMARY KEY, + profile_id BIGINT NOT NULL, + name VARCHAR(64) NOT NULL, + location_identifier BINARY(16) NOT NULL UNIQUE, + connected BOOLEAN NOT NULL DEFAULT false, + discoverable BOOLEAN NOT NULL DEFAULT true, + dht BOOLEAN NOT NULL DEFAULT true, + net_mode ENUM ('unknown', 'udp', 'upnp', 'ext', 'hidden', 'unreachable') DEFAULT 'unknown', + last_connected TIMESTAMP +); + +CREATE TABLE connections +( + id IDENTITY NOT NULL PRIMARY KEY, + location_id BIGINT NOT NULL, + type ENUM ('invalid', 'ipv4', 'ipv6', 'tor', 'i2p'), + address VARCHAR(128) NOT NULL, + last_connected TIMESTAMP, + external BOOLEAN NOT NULL +); + +CREATE TABLE identities +( + id IDENTITY NOT NULL PRIMARY KEY, + gxs_id BIGINT NOT NULL, + type ENUM ('signed', 'anonymous', 'friend') +); + +CREATE TABLE prefs +( + lock TINYINT NOT NULL DEFAULT 1, + + pgp_private_key_data VARBINARY(16384) DEFAULT NULL, + + location_private_key_data VARBINARY(16384) DEFAULT NULL, + location_public_key_data VARBINARY(16384) DEFAULT NULL, + location_certificate VARBINARY(16384) DEFAULT NULL, + + CONSTRAINT PK_T1 PRIMARY KEY (lock), + CONSTRAINT CK_T1_LOCKED CHECK (lock = 1) +); + +CREATE TABLE gxs_client_updates +( + id IDENTITY NOT NULL PRIMARY KEY, + location_id BIGINT NOT NULL, + service_type INT NOT NULL, + last_synced TIMESTAMP +); +CREATE INDEX idx_location_service ON gxs_client_updates (location_id, service_type); + +CREATE TABLE gxs_service_settings +( + id IDENTITY NOT NULL PRIMARY KEY, + last_updated TIMESTAMP +); + +CREATE TABLE gxs_groups +( + id IDENTITY NOT NULL PRIMARY KEY, + gxs_id BINARY(16) NOT NULL UNIQUE, + original_gxs_id BINARY(16), + name VARCHAR(512) NOT NULL, + diffusion_flags INT NOT NULL DEFAULT 0, + signature_flags INT NOT NULL DEFAULT 0, + published TIMESTAMP, + author BINARY(16), + circle_id BINARY(16), + circle_type ENUM ('unknown', 'public', 'external', 'your_friends_only', 'local', 'external_self', 'your_eyes_only') DEFAULT 'unknown', + authentication_flags INT NOT NULL DEFAULT 0, + parent_id BINARY(16), + subscribe_flags INT NOT NULL DEFAULT 0, + popularity INT NOT NULL DEFAULT 0, + visible_message_count INT NOT NULL DEFAULT 0, + last_posted TIMESTAMP, + status INT NOT NULL DEFAULT 0, + service_string VARCHAR(512), + originator BINARY(16), + internal_circle BINARY(16), + admin_private_key_data VARBINARY(16384), + admin_public_key_data VARBINARY(16384), + author_private_key_data VARBINARY(16384), + author_public_key_data VARBINARY(16384) +); + +CREATE TABLE gxs_id_groups +( + id IDENTITY NOT NULL PRIMARY KEY, + profile_hash BINARY(20), + profile_signature VARBINARY(2048) +); + +CREATE TABLE chatrooms +( + id IDENTITY NOT NULL PRIMARY KEY, + room_id BIGINT NOT NULL, + identity_id BIGINT NOT NULL, + name VARCHAR(256) NOT NULL, + topic VARCHAR(256) NOT NULL, + flags INT NOT NULL DEFAULT 0, + subscribed BOOLEAN NOT NULL DEFAULT true, + joined BOOLEAN NOT NULL DEFAULT false +); + +INSERT INTO prefs (lock) +VALUES (1); diff --git a/app/src/main/resources/public/index.html b/app/src/main/resources/public/index.html new file mode 100644 index 000000000..8e4a8f1b0 --- /dev/null +++ b/app/src/main/resources/public/index.html @@ -0,0 +1,34 @@ + + + + + + + Xeres Web Interface + + +This is the default interface. Either build the Angular Web App using the gradle target +
installAngular
+then +
buildAngular
+or just go to +the Swagger UI. + + \ No newline at end of file diff --git a/app/src/test/java/io/xeres/app/XeresApplicationTest.java b/app/src/test/java/io/xeres/app/XeresApplicationTest.java new file mode 100644 index 000000000..ae40bfc15 --- /dev/null +++ b/app/src/test/java/io/xeres/app/XeresApplicationTest.java @@ -0,0 +1,34 @@ +/* + * Copyright (c) 2019-2021 by David Gerber - https://zapek.com + * + * This file is part of Xeres. + * + * Xeres is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Xeres is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Xeres. If not, see . + */ + +package io.xeres.app; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.springframework.test.context.junit.jupiter.SpringExtension; + +@ExtendWith(SpringExtension.class) +class XeresApplicationTest +{ + @Test + void XeresApplication_ContextLoads_OK() + { + XeresApplication.main(new String[]{"--no-gui"}); + } +} diff --git a/app/src/test/java/io/xeres/app/application/SingleInstanceRunTest.java b/app/src/test/java/io/xeres/app/application/SingleInstanceRunTest.java new file mode 100644 index 000000000..a4e529346 --- /dev/null +++ b/app/src/test/java/io/xeres/app/application/SingleInstanceRunTest.java @@ -0,0 +1,32 @@ +/* + * Copyright (c) 2019-2021 by David Gerber - https://zapek.com + * + * This file is part of Xeres. + * + * Xeres is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Xeres is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Xeres. If not, see . + */ + +package io.xeres.app.application; + +import io.xeres.testutils.TestUtils; +import org.junit.jupiter.api.Test; + +class SingleInstanceRunTest +{ + @Test + void SingleInstanceRun_NoInstance_OK() throws NoSuchMethodException + { + TestUtils.assertUtilityClass(SingleInstanceRun.class); + } +} diff --git a/app/src/test/java/io/xeres/app/crypto/chatcipher/ChatChallengeTest.java b/app/src/test/java/io/xeres/app/crypto/chatcipher/ChatChallengeTest.java new file mode 100644 index 000000000..9469833e1 --- /dev/null +++ b/app/src/test/java/io/xeres/app/crypto/chatcipher/ChatChallengeTest.java @@ -0,0 +1,46 @@ +/* + * Copyright (c) 2019-2021 by David Gerber - https://zapek.com + * + * This file is part of Xeres. + * + * Xeres is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Xeres is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Xeres. If not, see . + */ + +package io.xeres.app.crypto.chatcipher; + +import io.xeres.common.id.GxsId; +import io.xeres.common.id.Id; +import io.xeres.testutils.TestUtils; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +class ChatChallengeTest +{ + @Test + void ChatChallenge_NoInstance_OK() throws NoSuchMethodException + { + TestUtils.assertUtilityClass(ChatChallenge.class); + } + + @Test + void ChatChallenge_Code_OK() + { + GxsId gxsId = new GxsId(Id.toBytes("325e3801988a347347ef3e5ae24a63ba")); + + long code = ChatChallenge.code(gxsId, 1, 2); + + assertEquals(749218228209201600L, code); + } +} diff --git a/app/src/test/java/io/xeres/app/crypto/pgp/PGPTest.java b/app/src/test/java/io/xeres/app/crypto/pgp/PGPTest.java new file mode 100644 index 000000000..968b34f26 --- /dev/null +++ b/app/src/test/java/io/xeres/app/crypto/pgp/PGPTest.java @@ -0,0 +1,148 @@ +/* + * Copyright (c) 2019-2021 by David Gerber - https://zapek.com + * + * This file is part of Xeres. + * + * Xeres is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Xeres is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Xeres. If not, see . + */ + +package io.xeres.app.crypto.pgp; + +import io.xeres.testutils.TestUtils; +import org.bouncycastle.bcpg.SymmetricKeyAlgorithmTags; +import org.bouncycastle.jce.provider.BouncyCastleProvider; +import org.bouncycastle.openpgp.PGPException; +import org.bouncycastle.openpgp.PGPSecretKey; +import org.bouncycastle.openpgp.operator.jcajce.JcePBESecretKeyDecryptorBuilder; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.security.InvalidKeyException; +import java.security.Security; +import java.security.SignatureException; + +import static io.xeres.app.crypto.pgp.PGP.*; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.junit.jupiter.api.Assertions.*; + +class PGPTest +{ + private static final int KEY_SIZE = 512; + private static PGPSecretKey pgpSecretKey; + + @BeforeAll + static void setup() throws PGPException + { + Security.addProvider(new BouncyCastleProvider()); + + pgpSecretKey = generateSecretKey("test", null, KEY_SIZE); + } + + @Test + void PGP_NoInstance_OK() throws NoSuchMethodException + { + TestUtils.assertUtilityClass(PGP.class); + } + + /** + * Generates a PGP secret key. + */ + @Test + void PGP_GenerateSecretKey_OK() throws PGPException + { + assertNotNull(pgpSecretKey); + assertTrue(pgpSecretKey.isMasterKey()); + assertTrue(pgpSecretKey.isSigningKey()); + assertFalse(pgpSecretKey.isPrivateKeyEmpty()); + assertEquals(SymmetricKeyAlgorithmTags.CAST5, pgpSecretKey.getKeyEncryptionAlgorithm()); + assertNotNull(pgpSecretKey.getPublicKey()); + assertNotNull(pgpSecretKey.extractPrivateKey(new JcePBESecretKeyDecryptorBuilder().setProvider("BC").build("".toCharArray()))); + } + + /** + * Signs using a PGP secret key then verifies. + */ + @Test + void PGP_Sign_OK() throws PGPException, IOException, SignatureException + { + byte[] in = "The lazy dog jumps over the drunk fox".getBytes(); + + var out = new ByteArrayOutputStream(); + + sign(pgpSecretKey, new ByteArrayInputStream(in), out, Armor.NONE); + + verify(pgpSecretKey.getPublicKey(), out.toByteArray(), new ByteArrayInputStream(in)); + } + + /** + * Signs using a PGP secret key then verifies with another. + */ + @Test + void PGP_Sign_Fail() throws PGPException, IOException + { + byte[] in = "The lazy dog jumps over the drunk fox".getBytes(); + + PGPSecretKey pgpSecretKey2 = generateSecretKey("test2", null, KEY_SIZE); + + var out = new ByteArrayOutputStream(); + + sign(pgpSecretKey, new ByteArrayInputStream(in), out, Armor.NONE); + + assertThatThrownBy(() -> verify(pgpSecretKey2.getPublicKey(), out.toByteArray(), new ByteArrayInputStream(in))) + .isInstanceOf(SignatureException.class); + } + + @Test + void PGP_GetSecretKey_OK() throws IOException + { + assertEquals(pgpSecretKey.getKeyID(), getPGPSecretKey(pgpSecretKey.getEncoded()).getKeyID()); + } + + @Test + void PGP_GetSecretKey_Corrupted_Fail() + { + assertThatThrownBy(() -> getPGPSecretKey(new byte[]{1, 2, 3})) + .isInstanceOf(IllegalArgumentException.class) + .hasMessageContaining("corrupted"); + } + + @Test + void PGP_GetPublicKey_OK() throws IOException, InvalidKeyException + { + assertEquals(pgpSecretKey.getPublicKey().getKeyID(), getPGPPublicKey(pgpSecretKey.getPublicKey().getEncoded()).getKeyID()); + } + + @Test + void PGP_GetPublicKey_Corrupted_Fail() + { + assertThatThrownBy(() -> getPGPPublicKey(new byte[]{1, 2, 3})) + .isInstanceOf(InvalidKeyException.class) + .hasMessageContaining("corrupted"); + } + + @Test + void PGP_GetPublicKeyArmored_OK() throws IOException + { + var out = new ByteArrayOutputStream(); + getPublicKeyArmored(pgpSecretKey.getPublicKey(), out); + + String output = out.toString(); + + assertTrue(output.contains("BEGIN PGP")); + assertTrue(output.contains("END PGP")); + } +} diff --git a/app/src/test/java/io/xeres/app/crypto/rsa/RSATest.java b/app/src/test/java/io/xeres/app/crypto/rsa/RSATest.java new file mode 100644 index 000000000..b3231b53c --- /dev/null +++ b/app/src/test/java/io/xeres/app/crypto/rsa/RSATest.java @@ -0,0 +1,104 @@ +/* + * Copyright (c) 2019-2021 by David Gerber - https://zapek.com + * + * This file is part of Xeres. + * + * Xeres is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Xeres is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Xeres. If not, see . + */ + +package io.xeres.app.crypto.rsa; + +import io.xeres.testutils.TestUtils; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; + +import java.security.KeyPair; +import java.security.NoSuchAlgorithmException; +import java.security.spec.InvalidKeySpecException; + +import static org.junit.jupiter.api.Assertions.*; + +class RSATest +{ + private static final int KEY_SIZE = 512; + + private static KeyPair keyPair; + + @BeforeAll + static void setup() + { + keyPair = RSA.generateKeys(KEY_SIZE); + } + + @Test + void RSA_NoInstance_OK() throws NoSuchMethodException + { + TestUtils.assertUtilityClass(RSA.class); + } + + /** + * Generates an RSA secret key. + */ + @Test + // tag::RSA_GenerateKeys_OK[] + void RSA_GenerateKeys_OK() + { + assertNotNull(keyPair); + assertEquals("RSA", keyPair.getPrivate().getAlgorithm()); + assertEquals("RSA", keyPair.getPublic().getAlgorithm()); + } + // end::RSA_GenerateKeys_OK[] + + @Test + void RSA_GetPrivateKey_OK() throws InvalidKeySpecException, NoSuchAlgorithmException + { + assertEquals(keyPair.getPrivate(), RSA.getPrivateKey(keyPair.getPrivate().getEncoded())); + } + + @Test + void RSA_GetPublicKey_OK() throws InvalidKeySpecException, NoSuchAlgorithmException + { + assertEquals(keyPair.getPublic(), RSA.getPublicKey(keyPair.getPublic().getEncoded())); + } + + @Test + void RSA_Sign_OK() + { + byte[] data = {1, 2, 3}; + + byte[] signature = RSA.sign(data, keyPair.getPrivate()); + + assertNotNull(signature); + + boolean result = RSA.verify(keyPair.getPublic(), signature, data); + + assertTrue(result); + } + + @Test + void RSA_Sign_TemperedData() + { + byte[] data = {1, 2, 3}; + + byte[] signature = RSA.sign(data, keyPair.getPrivate()); + + assertNotNull(signature); + + data[0] = 0; + + boolean result = RSA.verify(keyPair.getPublic(), signature, data); + + assertFalse(result); + } +} diff --git a/app/src/test/java/io/xeres/app/crypto/rsid/RSCertificateTest.java b/app/src/test/java/io/xeres/app/crypto/rsid/RSCertificateTest.java new file mode 100644 index 000000000..370e2aca2 --- /dev/null +++ b/app/src/test/java/io/xeres/app/crypto/rsid/RSCertificateTest.java @@ -0,0 +1,61 @@ +/* + * Copyright (c) 2019-2021 by David Gerber - https://zapek.com + * + * This file is part of Xeres. + * + * Xeres is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Xeres is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Xeres. If not, see . + */ + +package io.xeres.app.crypto.rsid; + +import org.junit.jupiter.api.Test; + +import java.security.cert.CertificateParsingException; + +import static org.junit.jupiter.api.Assertions.*; + +class RSCertificateTest +{ + @Test + void RSCertificate_Parse_OK() throws CertificateParsingException + { + String string = """ + CQEGAcGWxsBNBFpq3M0BCADEQWXjoNmUNDo/RSfYwlSavOQoTllnlLv7bmRHXRP2 + gRxBlCjp185VyI+mW9uWbNnv8TpMsScjKvS+x0uE3QoqjW9seSxq1hIu5ba3cDbU + 9CzhKfAyycreIWtjZn18IqfvQ3qg3yJ+JLYptA10AGO0ErCmMyhtXAeDthCD3JBa + M+jCXi0KGg5k2SkQq9OS+/ktD3/izLX5Zeo5z41s9pSRe5nGQd0vpcwSHTLCUK9P + 6okDXLNG5jjcLfHD6ap74oTb/My/XOCqprLHIcm00/Byabd9HsZ2Z63KK9ZJ8NCg + NwAX1dBwTx1dESdre7+GxUaE3aMYCBon2ZwsTvKv4mjPABEBAAHNInphcGVrIChH + ZW5lcmF0ZWQgYnkgUmV0cm9TaGFyZSkgPD7CwF8EEwECABMFAlpq3M0JEBM+UlCE + 3l1NAhkBAAD4mwf/RH/aoFKos9gNCOts9d8TcLhwzvIA++Ah2gmfBcdD9yS7bfiD + 2cR+qazwhl8GFuCldrUIs+oX0MpN7u2eBX26IH9qwszRQLsEgxETvTxc+0lSE/uz + 2j+YDQ3fU++ARu5/FKH6HwYspxE+NDnxnaqjkZNAtJmUUBnp9wW2LfkEvHLVnmIY + HIQQSSalA2yOzVd0Onf6WJJshctiBbglEZMViN3sypMeoYDct3qhGNCk0E3yojkE + zS/CSzueXKS2jucYaybaouACvQ/hlyJeGuv0Ba//lupYn6xRonNzuS8oMcJmUBfi + F9pVssvzvyfTIoyD8WGEI3COvthDhKDzF+5rOgIGVEvWwHwOAwap/kMmfA4EEmhv + bWUuZHluLnphcGVrLmNvbQYLTXkgY29tcHV0ZXIFEHql0D0fEzStg7Xe+nDb7wEH + AxnSjw=="""; + + RSId rsId = RSId.parse(string); + + assertNotNull(rsId); + assertNotNull(rsId.getPgpPublicKey()); + assertFalse(rsId.hasInternalIp()); // RS put 169.254.67.38 in my certificate... + assertNotNull(rsId.getInternalIp()); // which means it has an internal IP but it's invalid + assertFalse(rsId.getInternalIp().isValid()); + assertTrue(rsId.hasExternalIp()); + assertNotNull(rsId.getExternalIp()); + assertNotNull(rsId.getLocationId()); + } +} diff --git a/app/src/test/java/io/xeres/app/crypto/rsid/RSIdArmorTest.java b/app/src/test/java/io/xeres/app/crypto/rsid/RSIdArmorTest.java new file mode 100644 index 000000000..6f9796212 --- /dev/null +++ b/app/src/test/java/io/xeres/app/crypto/rsid/RSIdArmorTest.java @@ -0,0 +1,32 @@ +/* + * Copyright (c) 2019-2021 by David Gerber - https://zapek.com + * + * This file is part of Xeres. + * + * Xeres is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Xeres is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Xeres. If not, see . + */ + +package io.xeres.app.crypto.rsid; + +import io.xeres.testutils.TestUtils; +import org.junit.jupiter.api.Test; + +class RSIdArmorTest +{ + @Test + void RSIdArmor_NoInstance_OK() throws NoSuchMethodException + { + TestUtils.assertUtilityClass(RSIdArmor.class); + } +} diff --git a/app/src/test/java/io/xeres/app/crypto/rsid/RSIdCrcTest.java b/app/src/test/java/io/xeres/app/crypto/rsid/RSIdCrcTest.java new file mode 100644 index 000000000..23e13efba --- /dev/null +++ b/app/src/test/java/io/xeres/app/crypto/rsid/RSIdCrcTest.java @@ -0,0 +1,42 @@ +/* + * Copyright (c) 2019-2021 by David Gerber - https://zapek.com + * + * This file is part of Xeres. + * + * Xeres is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Xeres is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Xeres. If not, see . + */ + +package io.xeres.app.crypto.rsid; + +import io.xeres.testutils.TestUtils; +import org.junit.jupiter.api.Test; + +import static io.xeres.app.crypto.rsid.RSIdCrc.calculate24bitsCrc; +import static org.junit.jupiter.api.Assertions.assertEquals; + +class RSIdCrcTest +{ + @Test + void RSIdCrc_NoInstance_OK() throws NoSuchMethodException + { + TestUtils.assertUtilityClass(RSIdCrc.class); + } + + @Test + void RSIdCrc_CalculateCrc_OK() + { + var INPUT = "The quick brown fox jumps over the lazy dog".getBytes(); + assertEquals(10641804, calculate24bitsCrc(INPUT, INPUT.length)); + } +} diff --git a/app/src/test/java/io/xeres/app/crypto/rsid/RSSerialVersionTest.java b/app/src/test/java/io/xeres/app/crypto/rsid/RSSerialVersionTest.java new file mode 100644 index 000000000..f26def2bb --- /dev/null +++ b/app/src/test/java/io/xeres/app/crypto/rsid/RSSerialVersionTest.java @@ -0,0 +1,53 @@ +/* + * Copyright (c) 2019-2021 by David Gerber - https://zapek.com + * + * This file is part of Xeres. + * + * Xeres is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Xeres is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Xeres. If not, see . + */ + +package io.xeres.app.crypto.rsid; + +import org.junit.jupiter.api.Test; + +import java.math.BigInteger; +import java.util.concurrent.ThreadLocalRandom; + +import static io.xeres.app.crypto.rsid.RSSerialVersion.*; +import static org.junit.jupiter.api.Assertions.assertEquals; + +class RSSerialVersionTest +{ + @Test + void RSSerialVersion_Enum_Order() + { + assertEquals(0, V06_0000.ordinal()); + assertEquals(1, V06_0001.ordinal()); + assertEquals(2, V07_0001.ordinal()); + } + + @Test + void RSSerialVersion_GetFromSerialNumber_OK() + { + var RS_Old = new BigInteger(Integer.toString(ThreadLocalRandom.current().nextInt(100000, 2000000000)), 16); + var RS_6_4 = new BigInteger("60000", 16); + var RS_6_5 = new BigInteger("60001", 16); + var RS_7 = new BigInteger("70001", 16); + + assertEquals(V06_0000, RSSerialVersion.getFromSerialNumber(RS_6_4)); + assertEquals(RSSerialVersion.V06_0001, RSSerialVersion.getFromSerialNumber(RS_6_5)); + assertEquals(RSSerialVersion.V07_0001, RSSerialVersion.getFromSerialNumber(RS_7)); + assertEquals(V06_0000, RSSerialVersion.getFromSerialNumber(RS_Old)); + } +} diff --git a/app/src/test/java/io/xeres/app/crypto/rsid/RSShortInviteTest.java b/app/src/test/java/io/xeres/app/crypto/rsid/RSShortInviteTest.java new file mode 100644 index 000000000..cc5418c1a --- /dev/null +++ b/app/src/test/java/io/xeres/app/crypto/rsid/RSShortInviteTest.java @@ -0,0 +1,45 @@ +/* + * Copyright (c) 2019-2021 by David Gerber - https://zapek.com + * + * This file is part of Xeres. + * + * Xeres is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Xeres is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Xeres. If not, see . + */ + +package io.xeres.app.crypto.rsid; + +import org.junit.jupiter.api.Test; + +import java.security.cert.CertificateParsingException; + +import static org.junit.jupiter.api.Assertions.*; + +class RSShortInviteTest +{ + @Test + void ShortInvite_Parse_OK() throws CertificateParsingException + { + String string = "\nABB6pdA9HxM0rYO13vpw2+8BAxT1dVEUN811dCoiyW0TPlJQhN5dTQEFemFwZWuSBv7Zw1UhUAQDOuCY\n"; // XXX: try it with a later ID too! + + RSId rsId = RSId.parse(string); + + assertNotNull(rsId); + assertNull(rsId.getPgpPublicKey()); + assertFalse(rsId.hasInternalIp()); // XXX: try with a later ID... which has the right IP in the certificate + assertNull(rsId.getInternalIp()); + assertTrue(rsId.hasExternalIp()); + assertNotNull(rsId.getExternalIp()); + assertNotNull(rsId.getLocationId()); + } +} diff --git a/app/src/test/java/io/xeres/app/crypto/rsid/certificate/RSCertificateFakes.java b/app/src/test/java/io/xeres/app/crypto/rsid/certificate/RSCertificateFakes.java new file mode 100644 index 000000000..286e68494 --- /dev/null +++ b/app/src/test/java/io/xeres/app/crypto/rsid/certificate/RSCertificateFakes.java @@ -0,0 +1,36 @@ +/* + * Copyright (c) 2019-2020 by David Gerber - https://zapek.com + * + * This file is part of Xeres. + * + * Xeres is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Xeres is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Xeres. If not, see . + */ + +package io.xeres.app.crypto.rsid.certificate; + +import java.io.IOException; +import java.security.cert.CertificateParsingException; + +public final class RSCertificateFakes +{ + private RSCertificateFakes() + { + throw new UnsupportedOperationException("Utility class"); + } + + public static RSCertificate createRSCertificate() throws IOException, CertificateParsingException + { + return new RSCertificate(); + } +} diff --git a/app/src/test/java/io/xeres/app/crypto/rsid/shortinvite/ShortInviteQuirksTest.java b/app/src/test/java/io/xeres/app/crypto/rsid/shortinvite/ShortInviteQuirksTest.java new file mode 100644 index 000000000..816b365ee --- /dev/null +++ b/app/src/test/java/io/xeres/app/crypto/rsid/shortinvite/ShortInviteQuirksTest.java @@ -0,0 +1,53 @@ +/* + * Copyright (c) 2019-2021 by David Gerber - https://zapek.com + * + * This file is part of Xeres. + * + * Xeres is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Xeres is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Xeres. If not, see . + */ + +package io.xeres.app.crypto.rsid.shortinvite; + +import io.xeres.testutils.TestUtils; +import org.junit.jupiter.api.Test; + +import static io.xeres.app.crypto.rsid.shortinvite.ShortInviteQuirks.swapBytes; +import static org.junit.jupiter.api.Assertions.assertArrayEquals; + +class ShortInviteQuirksTest +{ + @Test + void ShortInviteQuirks_NoInstance_OK() throws NoSuchMethodException + { + TestUtils.assertUtilityClass(ShortInviteQuirks.class); + } + + @Test + void ShortInviteQuirks_SwapBytes_OK() + { + var INPUT = new byte[]{1, 2, 3, 4, 5, 6}; + var OUTPUT = new byte[]{4, 3, 2, 1, 5, 6}; + + assertArrayEquals(OUTPUT, swapBytes(INPUT)); + } + + @Test + void ShortInviteQuirks_SwapBytes_WrongInput_NoSwap() + { + var INPUT = new byte[]{1, 2, 3, 4, 5, 6, 7}; + var OUTPUT = new byte[]{1, 2, 3, 4, 5, 6, 7}; + + assertArrayEquals(OUTPUT, swapBytes(INPUT)); + } +} diff --git a/app/src/test/java/io/xeres/app/crypto/rsid/shortinvite/ShortInviteTagsTest.java b/app/src/test/java/io/xeres/app/crypto/rsid/shortinvite/ShortInviteTagsTest.java new file mode 100644 index 000000000..8340e6b3d --- /dev/null +++ b/app/src/test/java/io/xeres/app/crypto/rsid/shortinvite/ShortInviteTagsTest.java @@ -0,0 +1,49 @@ +/* + * Copyright (c) 2019-2021 by David Gerber - https://zapek.com + * + * This file is part of Xeres. + * + * Xeres is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Xeres is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Xeres. If not, see . + */ + +package io.xeres.app.crypto.rsid.shortinvite; + +import io.xeres.testutils.TestUtils; +import org.junit.jupiter.api.Test; + +import static io.xeres.app.crypto.rsid.shortinvite.ShortInviteTags.*; +import static org.junit.jupiter.api.Assertions.assertEquals; + +class ShortInviteTagsTest +{ + @Test + void ShortInviteTags_NoInstance_OK() throws NoSuchMethodException + { + TestUtils.assertUtilityClass(ShortInviteTags.class); + } + + @Test + void ShortInviteTags_Values() + { + assertEquals(0x0, SSLID); + assertEquals(0x1, NAME); + assertEquals(0x2, LOCATOR); + assertEquals(0x3, PGP_FINGERPRINT); + assertEquals(0X4, CHECKSUM); + assertEquals(0X90, HIDDEN_LOCATOR); + assertEquals(0X91, DNS_LOCATOR); + assertEquals(0X92, EXT4_LOCATOR); + assertEquals(0X93, LOC4_LOCATOR); + } +} diff --git a/app/src/test/java/io/xeres/app/crypto/scramble/ScrambledStringTest.java b/app/src/test/java/io/xeres/app/crypto/scramble/ScrambledStringTest.java new file mode 100644 index 000000000..58f014dfc --- /dev/null +++ b/app/src/test/java/io/xeres/app/crypto/scramble/ScrambledStringTest.java @@ -0,0 +1,102 @@ +/* + * Copyright (c) 2019-2021 by David Gerber - https://zapek.com + * + * This file is part of Xeres. + * + * Xeres is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Xeres is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Xeres. If not, see . + */ + +package io.xeres.app.crypto.scramble; + +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.*; + +class ScrambledStringTest +{ + @Test + void ScrambledString_Constructor_Empty_OK() + { + var ss = new ScrambledString(); + + assertEquals("[SCRAMBLED]", ss.toString()); + } + + @Test + void ScrambledString_Constructor_OK() + { + String TEST = "1234"; + String HASH = "A6xnQhbz4Vx2HuGl4lXwZ5U2I8iziLRFnhP5eNfIRvQ="; + var ss = new ScrambledString(TEST.toCharArray()); + + assertEquals("[SCRAMBLED]", ss.toString()); + assertEquals(HASH, ss.getBase64SHA256Hash()); + assertTrue(ss.verifyBase64SHA256Hash(HASH)); + ss.access(clearChars -> assertArrayEquals(TEST.toCharArray(), clearChars)); + } + + @Test + void ScrambledString_Append_OK() + { + String TEST = "1234"; + String HASH = "A6xnQhbz4Vx2HuGl4lXwZ5U2I8iziLRFnhP5eNfIRvQ="; + + var ss = new ScrambledString(); + + for (char c : TEST.toCharArray()) + { + ss.appendChar(c); + } + assertTrue(ss.verifyBase64SHA256Hash(HASH)); + } + + @Test + void ScrambledString_Dispose_OK() + { + String TEST = "1234"; + + var ss = new ScrambledString(TEST.toCharArray()); + + ss.dispose(); + + assertThrows(IllegalStateException.class, () -> ss.access(System.out::print)); + assertThrows(IllegalStateException.class, () -> ss.appendChar('a')); + assertThrows(IllegalStateException.class, () -> ss.verifyBase64SHA256Hash("a")); + assertThrows(IllegalStateException.class, () -> System.out.println(ss.getBase64SHA256Hash())); + assertEquals("", ss.toString()); + } + + @Test + void ScrambledString_Equality_OK() + { + String TEST = "1234"; + + var ss1 = new ScrambledString(TEST.toCharArray()); + var ss2 = new ScrambledString(TEST.toCharArray()); + + assertEquals(ss1, ss2); + } + + @Test + void ScrambledString_Equality_Fail() + { + String TEST1 = "1234"; + String TEST2 = "5678"; + + var ss1 = new ScrambledString(TEST1.toCharArray()); + var ss2 = new ScrambledString(TEST2.toCharArray()); + + assertNotEquals(ss1, ss2); + } +} diff --git a/app/src/test/java/io/xeres/app/crypto/x509/X509Test.java b/app/src/test/java/io/xeres/app/crypto/x509/X509Test.java new file mode 100644 index 000000000..9469eec04 --- /dev/null +++ b/app/src/test/java/io/xeres/app/crypto/x509/X509Test.java @@ -0,0 +1,105 @@ +/* + * Copyright (c) 2019-2021 by David Gerber - https://zapek.com + * + * This file is part of Xeres. + * + * Xeres is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Xeres is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Xeres. If not, see . + */ + +package io.xeres.app.crypto.x509; + +import io.xeres.app.crypto.pgp.PGP; +import io.xeres.app.crypto.rsa.RSA; +import io.xeres.app.crypto.rsid.RSSerialVersion; +import io.xeres.testutils.TestUtils; +import org.bouncycastle.jce.provider.BouncyCastleProvider; +import org.bouncycastle.openpgp.PGPException; +import org.bouncycastle.openpgp.PGPSecretKey; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; + +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.math.BigInteger; +import java.security.KeyPair; +import java.security.Security; +import java.security.SignatureException; +import java.security.cert.CertificateException; +import java.security.cert.X509Certificate; +import java.util.Date; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; + +class X509Test +{ + private static final int KEY_SIZE = 512; + private static PGPSecretKey pgpSecretKey; + private static KeyPair keyPair; + + @BeforeAll + static void setup() throws PGPException + { + Security.addProvider(new BouncyCastleProvider()); + + pgpSecretKey = PGP.generateSecretKey("test", null, KEY_SIZE); + keyPair = RSA.generateKeys(KEY_SIZE); + } + + @Test + void X509_NoInstance_OK() throws NoSuchMethodException + { + TestUtils.assertUtilityClass(X509.class); + } + + /** + * Generates an X509 certificate. + */ + @Test + // tag::X509_GenerateCertificate_OK[] + void X509_GenerateCertificate_OK() throws PGPException, IOException, CertificateException, SignatureException + { + generateCertificate(RSSerialVersion.V07_0001.serialNumber()); + } + // end::X509_GenerateCertificate_OK[] + + @Test + void X509_GenerateCertificate_OldRS_0_6_5_OK() throws PGPException, IOException, CertificateException, SignatureException + { + generateCertificate(RSSerialVersion.V06_0001.serialNumber()); + } + + @Test + void X509_GenerateCertificate_OldestRS_OK() throws PGPException, IOException, CertificateException, SignatureException + { + generateCertificate(new BigInteger("123456", 16)); + } + + private void generateCertificate(BigInteger serialNumber) throws IOException, CertificateException, PGPException, SignatureException + { + String issuer = "CN=1234"; + String subject = "CN=-"; + var from = new Date(0); + var to = new Date(0); + + X509Certificate cert = X509.generateCertificate(pgpSecretKey, keyPair.getPublic(), issuer, subject, from, to, serialNumber); + assertNotNull(cert); + assertEquals(issuer, cert.getIssuerX500Principal().getName()); + assertEquals(subject, cert.getSubjectX500Principal().getName()); + assertEquals(serialNumber, cert.getSerialNumber()); + assertEquals(from, cert.getNotBefore()); + assertEquals(to, cert.getNotAfter()); + PGP.verify(pgpSecretKey.getPublicKey(), cert.getSignature(), new ByteArrayInputStream(cert.getTBSCertificate())); + } +} diff --git a/app/src/test/java/io/xeres/app/database/model/chatroom/ChatRoomFakes.java b/app/src/test/java/io/xeres/app/database/model/chatroom/ChatRoomFakes.java new file mode 100644 index 000000000..8dc6e2ae5 --- /dev/null +++ b/app/src/test/java/io/xeres/app/database/model/chatroom/ChatRoomFakes.java @@ -0,0 +1,50 @@ +/* + * Copyright (c) 2019-2021 by David Gerber - https://zapek.com + * + * This file is part of Xeres. + * + * Xeres is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Xeres is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Xeres. If not, see . + */ + +package io.xeres.app.database.model.chatroom; + +import io.xeres.app.database.model.identity.Identity; +import io.xeres.app.database.model.identity.IdentityFakes; +import io.xeres.common.identity.Type; +import org.apache.commons.lang3.RandomStringUtils; + +import java.util.concurrent.ThreadLocalRandom; + +public final class ChatRoomFakes +{ + private ChatRoomFakes() + { + throw new UnsupportedOperationException("Utility class"); + } + + public static ChatRoom createChatRoom() + { + return createChatRoom(ThreadLocalRandom.current().nextLong(), IdentityFakes.createOwnIdentity("test", Type.SIGNED), RandomStringUtils.randomAlphabetic(8), RandomStringUtils.randomAlphabetic(8), 0); + } + + public static ChatRoom createChatRoom(Identity identity) + { + return createChatRoom(ThreadLocalRandom.current().nextLong(), identity, RandomStringUtils.randomAlphabetic(8), RandomStringUtils.randomAlphabetic(8), 0); + } + + public static ChatRoom createChatRoom(long roomId, Identity identity, String name, String topic, int flags) + { + return new ChatRoom(roomId, identity, name, topic, flags); + } +} diff --git a/app/src/test/java/io/xeres/app/database/model/connection/ConnectionFakes.java b/app/src/test/java/io/xeres/app/database/model/connection/ConnectionFakes.java new file mode 100644 index 000000000..6ccea7209 --- /dev/null +++ b/app/src/test/java/io/xeres/app/database/model/connection/ConnectionFakes.java @@ -0,0 +1,43 @@ +/* + * Copyright (c) 2019-2020 by David Gerber - https://zapek.com + * + * This file is part of Xeres. + * + * Xeres is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Xeres is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Xeres. If not, see . + */ + +package io.xeres.app.database.model.connection; + +import io.xeres.app.net.protocol.PeerAddress; + +public final class ConnectionFakes +{ + private ConnectionFakes() + { + throw new UnsupportedOperationException("Utility class"); + } + + public static Connection createConnection() + { + return createConnection(PeerAddress.Type.IPV4, "85.12.32.11:2022"); + } + + public static Connection createConnection(PeerAddress.Type type, String address) + { + var connection = new Connection(); + connection.setType(type); + connection.setAddress(address); + return connection; + } +} diff --git a/app/src/test/java/io/xeres/app/database/model/connection/ConnectionMapperTest.java b/app/src/test/java/io/xeres/app/database/model/connection/ConnectionMapperTest.java new file mode 100644 index 000000000..d258e3f73 --- /dev/null +++ b/app/src/test/java/io/xeres/app/database/model/connection/ConnectionMapperTest.java @@ -0,0 +1,67 @@ +/* + * Copyright (c) 2019-2021 by David Gerber - https://zapek.com + * + * This file is part of Xeres. + * + * Xeres is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Xeres is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Xeres. If not, see . + */ + +package io.xeres.app.database.model.connection; + +import io.xeres.common.dto.connection.ConnectionDTO; +import io.xeres.testutils.TestUtils; +import org.junit.jupiter.api.Test; + +import java.time.Instant; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +class ConnectionMapperTest +{ + @Test + void ConnectionMapper_NoInstance_OK() throws NoSuchMethodException + { + TestUtils.assertUtilityClass(ConnectionMapper.class); + } + + @Test + void ConnectionMapper_toDTO_OK() + { + var connection = ConnectionFakes.createConnection(); + var connectionDTO = ConnectionMapper.toDTO(connection); + + assertEquals(connection.getId(), connectionDTO.id()); + assertEquals(connection.getAddress(), connectionDTO.address()); + assertEquals(connection.getLastConnected(), connectionDTO.lastConnected()); + assertEquals(connection.isExternal(), connectionDTO.external()); + } + + @Test + void ConnectionMapper_fromDTO_OK() + { + var connectionDTO = new ConnectionDTO( + 1L, + "85.11.11.12", + Instant.now(), + true + ); + + var connection = ConnectionMapper.fromDTO(connectionDTO); + + assertEquals(connectionDTO.id(), connection.getId()); + assertEquals(connectionDTO.address(), connection.getAddress()); + assertEquals(connectionDTO.external(), connection.isExternal()); + assertEquals(connectionDTO.lastConnected(), connection.getLastConnected()); + } +} diff --git a/app/src/test/java/io/xeres/app/database/model/gxs/GxsCircleTypeTest.java b/app/src/test/java/io/xeres/app/database/model/gxs/GxsCircleTypeTest.java new file mode 100644 index 000000000..0807ffa44 --- /dev/null +++ b/app/src/test/java/io/xeres/app/database/model/gxs/GxsCircleTypeTest.java @@ -0,0 +1,40 @@ +/* + * Copyright (c) 2019-2021 by David Gerber - https://zapek.com + * + * This file is part of Xeres. + * + * Xeres is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Xeres is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Xeres. If not, see . + */ + +package io.xeres.app.database.model.gxs; + +import org.junit.jupiter.api.Test; + +import static io.xeres.app.database.model.gxs.GxsCircleType.*; +import static org.junit.jupiter.api.Assertions.assertEquals; + +class GxsCircleTypeTest +{ + @Test + void GxsCircleType_Enum_Order() + { + assertEquals(0, UNKNOWN.ordinal()); + assertEquals(1, PUBLIC.ordinal()); + assertEquals(2, EXTERNAL.ordinal()); + assertEquals(3, YOUR_FRIENDS_ONLY.ordinal()); + assertEquals(4, LOCAL.ordinal()); + assertEquals(5, EXTERNAL_SELF.ordinal()); + assertEquals(6, YOUR_EYES_ONLY.ordinal()); + } +} diff --git a/app/src/test/java/io/xeres/app/database/model/gxs/GxsIdGroupItemFakes.java b/app/src/test/java/io/xeres/app/database/model/gxs/GxsIdGroupItemFakes.java new file mode 100644 index 000000000..1b23644d4 --- /dev/null +++ b/app/src/test/java/io/xeres/app/database/model/gxs/GxsIdGroupItemFakes.java @@ -0,0 +1,52 @@ +/* + * Copyright (c) 2019-2021 by David Gerber - https://zapek.com + * + * This file is part of Xeres. + * + * Xeres is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Xeres is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Xeres. If not, see . + */ + +package io.xeres.app.database.model.gxs; + +import io.xeres.app.xrs.service.gxsid.item.GxsIdGroupItem; +import io.xeres.common.id.GxsId; +import io.xeres.common.id.Sha1Sum; +import org.apache.commons.lang3.RandomStringUtils; +import org.apache.commons.lang3.RandomUtils; + +import java.time.Instant; +import java.util.EnumSet; + +public final class GxsIdGroupItemFakes +{ + private GxsIdGroupItemFakes() + { + throw new UnsupportedOperationException("Utility class"); + } + + public static GxsIdGroupItem createGxsIdGroupItem() + { + return createGxsIdGroupItem(new GxsId(RandomUtils.nextBytes(16)), RandomStringUtils.randomAlphabetic(8)); + } + + public static GxsIdGroupItem createGxsIdGroupItem(GxsId gxsId, String name) + { + var item = new GxsIdGroupItem(gxsId, name); + item.setDiffusionFlags(EnumSet.noneOf(GxsPrivacyFlags.class)); + item.setSignatureFlags(EnumSet.noneOf(GxsSignatureFlags.class)); + item.setPublished(Instant.now()); + item.setProfileHash(new Sha1Sum(new byte[]{0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19})); + return item; + } +} diff --git a/app/src/test/java/io/xeres/app/database/model/gxs/GxsPrivacyFlagsTest.java b/app/src/test/java/io/xeres/app/database/model/gxs/GxsPrivacyFlagsTest.java new file mode 100644 index 000000000..ab2192c1b --- /dev/null +++ b/app/src/test/java/io/xeres/app/database/model/gxs/GxsPrivacyFlagsTest.java @@ -0,0 +1,37 @@ +/* + * Copyright (c) 2019-2021 by David Gerber - https://zapek.com + * + * This file is part of Xeres. + * + * Xeres is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Xeres is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Xeres. If not, see . + */ + +package io.xeres.app.database.model.gxs; + +import org.junit.jupiter.api.Test; + +import static io.xeres.app.database.model.gxs.GxsPrivacyFlags.*; +import static org.junit.jupiter.api.Assertions.assertEquals; + +class GxsPrivacyFlagsTest +{ + @Test + void GxsPrivacyFlagsTest_Enum_Order() + { + assertEquals(0, PRIVATE.ordinal()); + assertEquals(1, RESTRICTED.ordinal()); + assertEquals(2, PUBLIC.ordinal()); + assertEquals(8, READ_ID.ordinal()); + } +} diff --git a/app/src/test/java/io/xeres/app/database/model/gxs/GxsSignatureFlagsTest.java b/app/src/test/java/io/xeres/app/database/model/gxs/GxsSignatureFlagsTest.java new file mode 100644 index 000000000..3389c2a84 --- /dev/null +++ b/app/src/test/java/io/xeres/app/database/model/gxs/GxsSignatureFlagsTest.java @@ -0,0 +1,37 @@ +/* + * Copyright (c) 2019-2021 by David Gerber - https://zapek.com + * + * This file is part of Xeres. + * + * Xeres is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Xeres is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Xeres. If not, see . + */ + +package io.xeres.app.database.model.gxs; + +import org.junit.jupiter.api.Test; + +import static io.xeres.app.database.model.gxs.GxsSignatureFlags.*; +import static org.junit.jupiter.api.Assertions.assertEquals; + +class GxsSignatureFlagsTest +{ + @Test + void GxsSignatureFlags_Enum_Order() + { + assertEquals(0, ENCRYPTED.ordinal()); + assertEquals(1, ALL_SIGNED.ordinal()); + assertEquals(2, THREAD_HEAD.ordinal()); + assertEquals(3, NONE_REQUIRED.ordinal()); + } +} diff --git a/app/src/test/java/io/xeres/app/database/model/identity/IdentityFakes.java b/app/src/test/java/io/xeres/app/database/model/identity/IdentityFakes.java new file mode 100644 index 000000000..d87070541 --- /dev/null +++ b/app/src/test/java/io/xeres/app/database/model/identity/IdentityFakes.java @@ -0,0 +1,49 @@ +/* + * Copyright (c) 2019-2021 by David Gerber - https://zapek.com + * + * This file is part of Xeres. + * + * Xeres is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Xeres is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Xeres. If not, see . + */ + +package io.xeres.app.database.model.identity; + +import io.xeres.app.xrs.service.gxsid.item.GxsIdGroupItem; +import io.xeres.common.id.GxsId; +import io.xeres.common.id.Id; +import io.xeres.common.identity.Type; + +import static io.xeres.common.dto.identity.IdentityConstants.OWN_IDENTITY_ID; + +public final class IdentityFakes +{ + private IdentityFakes() + { + throw new UnsupportedOperationException("Utility class"); + } + + private static long id = OWN_IDENTITY_ID + 1; + + private static long getUniqueId() + { + return id++; + } + + public static Identity createOwnIdentity(String name, Type type) + { + var identity = Identity.createOwnIdentity(new GxsIdGroupItem(new GxsId(Id.toBytes("325e3801988a347347ef3e5ae24a63ba")), name), type); + identity.setId(getUniqueId()); + return identity; + } +} diff --git a/app/src/test/java/io/xeres/app/database/model/location/LocationFakes.java b/app/src/test/java/io/xeres/app/database/model/location/LocationFakes.java new file mode 100644 index 000000000..022c1986f --- /dev/null +++ b/app/src/test/java/io/xeres/app/database/model/location/LocationFakes.java @@ -0,0 +1,75 @@ +/* + * Copyright (c) 2019-2020 by David Gerber - https://zapek.com + * + * This file is part of Xeres. + * + * Xeres is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Xeres is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Xeres. If not, see . + */ + +package io.xeres.app.database.model.location; + +import io.xeres.app.database.model.profile.Profile; +import io.xeres.app.database.model.profile.ProfileFakes; +import io.xeres.common.id.LocationId; +import io.xeres.common.protocol.NetMode; +import org.apache.commons.lang3.RandomStringUtils; + +import java.util.concurrent.ThreadLocalRandom; + +import static io.xeres.common.dto.location.LocationConstants.OWN_LOCATION_ID; + +public final class LocationFakes +{ + private LocationFakes() + { + throw new UnsupportedOperationException("Utility class"); + } + + private static long id = OWN_LOCATION_ID + 1; + + private static long getUniqueId() + { + return id++; + } + + public static Location createOwnLocation() + { + return new Location(OWN_LOCATION_ID, RandomStringUtils.randomAlphabetic(8), ProfileFakes.createProfile(), new LocationId(getRandomArray())); + } + + public static Location createLocation() + { + return createLocation(RandomStringUtils.randomAlphabetic(8), ProfileFakes.createProfile(), new LocationId(getRandomArray())); + } + + public static Location createLocation(String name, Profile profile) + { + return createLocation(name, profile, new LocationId(getRandomArray())); + } + + public static Location createLocation(String name, Profile profile, LocationId locationId) + { + var location = new Location(getUniqueId(), name, profile, locationId); + location.setNetMode(NetMode.UPNP); + location.setVersion("Xeres 0.1.1"); + return location; + } + + private static byte[] getRandomArray() + { + var a = new byte[16]; + ThreadLocalRandom.current().nextBytes(a); + return a; + } +} diff --git a/app/src/test/java/io/xeres/app/database/model/location/LocationMapperTest.java b/app/src/test/java/io/xeres/app/database/model/location/LocationMapperTest.java new file mode 100644 index 000000000..51009bf86 --- /dev/null +++ b/app/src/test/java/io/xeres/app/database/model/location/LocationMapperTest.java @@ -0,0 +1,87 @@ +/* + * Copyright (c) 2019-2021 by David Gerber - https://zapek.com + * + * This file is part of Xeres. + * + * Xeres is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Xeres is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Xeres. If not, see . + */ + +package io.xeres.app.database.model.location; + +import io.xeres.app.database.model.connection.ConnectionFakes; +import io.xeres.app.database.model.profile.ProfileFakes; +import io.xeres.common.dto.location.LocationDTO; +import io.xeres.testutils.TestUtils; +import org.junit.jupiter.api.Test; + +import java.time.Instant; + +import static org.junit.jupiter.api.Assertions.assertArrayEquals; +import static org.junit.jupiter.api.Assertions.assertEquals; + +class LocationMapperTest +{ + @Test + void LocationMapper_NoInstance_OK() throws NoSuchMethodException + { + TestUtils.assertUtilityClass(LocationMapper.class); + } + + @Test + void LocationMapper_toDTO_OK() + { + var location = LocationFakes.createLocation("test", ProfileFakes.createProfile("test", 1)); + var locationDTO = LocationMapper.toDTO(location); + + assertEquals(location.getId(), locationDTO.id()); + assertEquals(location.getName(), locationDTO.name()); + assertArrayEquals(location.getLocationId().getBytes(), locationDTO.locationIdentifier()); + assertEquals(location.isConnected(), locationDTO.connected()); + assertEquals(location.getLastConnected(), locationDTO.lastConnected()); + } + + @Test + void LocationMapper_toDeepDTO_OK() + { + var location = LocationFakes.createLocation("test", ProfileFakes.createProfile("test", 1)); + location.addConnection(ConnectionFakes.createConnection()); + + var locationDTO = LocationMapper.toDeepDTO(location); + + assertEquals(location.getId(), locationDTO.id()); + assertEquals(location.getConnections().get(0).getAddress(), locationDTO.connections().get(0).address()); + } + + @Test + void LocationMapper_fromDTO_OK() + { + var locationDTO = new LocationDTO( + 1L, + "test", + new byte[16], + "foo", + null, + true, + Instant.now() + ); + + var location = LocationMapper.fromDTO(locationDTO); + + assertEquals(locationDTO.id(), location.getId()); + assertEquals(locationDTO.name(), location.getName()); + assertArrayEquals(locationDTO.locationIdentifier(), location.getLocationId().getBytes()); + assertEquals(locationDTO.connected(), location.isConnected()); + assertEquals(locationDTO.lastConnected(), location.getLastConnected()); + } +} diff --git a/app/src/test/java/io/xeres/app/database/model/prefs/PrefsFakes.java b/app/src/test/java/io/xeres/app/database/model/prefs/PrefsFakes.java new file mode 100644 index 000000000..541988bde --- /dev/null +++ b/app/src/test/java/io/xeres/app/database/model/prefs/PrefsFakes.java @@ -0,0 +1,47 @@ +/* + * Copyright (c) 2019-2020 by David Gerber - https://zapek.com + * + * This file is part of Xeres. + * + * Xeres is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Xeres is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Xeres. If not, see . + */ + +package io.xeres.app.database.model.prefs; + +import java.util.concurrent.ThreadLocalRandom; + +public final class PrefsFakes +{ + private PrefsFakes() + { + throw new UnsupportedOperationException("Utility class"); + } + + public static Prefs createPrefs() + { + var prefs = new Prefs(); + prefs.setPgpPrivateKeyData(getRandomArray(2000)); + prefs.setLocationPrivateKeyData(getRandomArray(2000)); + prefs.setLocationPublicKeyData(getRandomArray(500)); + prefs.setLocationCertificate(getRandomArray(200)); + return prefs; + } + + private static byte[] getRandomArray(int size) + { + var a = new byte[size]; + ThreadLocalRandom.current().nextBytes(a); + return a; + } +} diff --git a/app/src/test/java/io/xeres/app/database/model/profile/ProfileFakes.java b/app/src/test/java/io/xeres/app/database/model/profile/ProfileFakes.java new file mode 100644 index 000000000..14a04e510 --- /dev/null +++ b/app/src/test/java/io/xeres/app/database/model/profile/ProfileFakes.java @@ -0,0 +1,69 @@ +/* + * Copyright (c) 2019-2020 by David Gerber - https://zapek.com + * + * This file is part of Xeres. + * + * Xeres is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Xeres is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Xeres. If not, see . + */ + +package io.xeres.app.database.model.profile; + +import io.xeres.common.id.ProfileFingerprint; +import org.apache.commons.lang3.RandomStringUtils; + +import java.util.concurrent.ThreadLocalRandom; + +import static io.xeres.common.dto.profile.ProfileConstants.OWN_PROFILE_ID; + +public final class ProfileFakes +{ + private ProfileFakes() + { + throw new UnsupportedOperationException("Utility class"); + } + + private static long id = OWN_PROFILE_ID + 1; + + private static long getUniqueId() + { + return id++; + } + + public static Profile createProfile() + { + return createProfile(RandomStringUtils.randomAlphabetic(8), ThreadLocalRandom.current().nextLong()); + } + + public static Profile createProfile(String name, long pgpIdentifier) + { + return createProfile(name, pgpIdentifier, new ProfileFingerprint(getRandomArray(20)), getRandomArray(200)); + } + + public static Profile createProfile(String name, long pgpIdentifier, byte[] pgpFingerprint, byte[] data) + { + return new Profile(getUniqueId(), name, pgpIdentifier, new ProfileFingerprint(pgpFingerprint), data); + } + + public static Profile createProfile(String name, long pgpIdentifier, ProfileFingerprint profileFingerprint, byte[] data) + { + return new Profile(getUniqueId(), name, pgpIdentifier, profileFingerprint, data); + } + + private static byte[] getRandomArray(int size) + { + var a = new byte[size]; + ThreadLocalRandom.current().nextBytes(a); + return a; + } +} diff --git a/app/src/test/java/io/xeres/app/database/model/profile/ProfileMapperTest.java b/app/src/test/java/io/xeres/app/database/model/profile/ProfileMapperTest.java new file mode 100644 index 000000000..bc2ede471 --- /dev/null +++ b/app/src/test/java/io/xeres/app/database/model/profile/ProfileMapperTest.java @@ -0,0 +1,90 @@ +/* + * Copyright (c) 2019-2021 by David Gerber - https://zapek.com + * + * This file is part of Xeres. + * + * Xeres is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Xeres is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Xeres. If not, see . + */ + +package io.xeres.app.database.model.profile; + +import io.xeres.app.database.model.location.LocationFakes; +import io.xeres.common.dto.profile.ProfileDTO; +import io.xeres.common.pgp.Trust; +import io.xeres.testutils.TestUtils; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertArrayEquals; +import static org.junit.jupiter.api.Assertions.assertEquals; + +class ProfileMapperTest +{ + @Test + void ProfileMapper_NoInstance_OK() throws NoSuchMethodException + { + TestUtils.assertUtilityClass(ProfileMapper.class); + } + + @Test + void ProfileMapper_toDTO_OK() + { + var profile = ProfileFakes.createProfile("test", 1); + var profileDTO = ProfileMapper.toDTO(profile); + + assertEquals(profile.getId(), profileDTO.id()); + assertEquals(profile.getName(), profileDTO.name()); + assertEquals(profile.getPgpIdentifier(), Long.parseLong(profileDTO.pgpIdentifier())); + assertArrayEquals(profile.getProfileFingerprint().getBytes(), profileDTO.pgpFingerprint()); + assertArrayEquals(profile.getPgpPublicKeyData(), profileDTO.pgpPublicKeyData()); + assertEquals(profile.isAccepted(), profileDTO.accepted()); + assertEquals(profile.getTrust(), profileDTO.trust()); + } + + @Test + void ProfileMapper_toDeepDTO_OK() + { + var profile = ProfileFakes.createProfile("test", 1); + profile.addLocation(LocationFakes.createLocation("foo", profile)); + + var profileDTO = ProfileMapper.toDeepDTO(profile); + + assertEquals(profile.getId(), profileDTO.id()); + assertEquals(profile.getLocations().get(0).getId(), profileDTO.locations().get(0).id()); + } + + @Test + void ProfileMapper_fromDTO_OK() + { + var profileDTO = new ProfileDTO( + 1L, + "prout", + "2", + new byte[20], + new byte[4], + true, + Trust.ULTIMATE, + null + ); + + var profile = ProfileMapper.fromDTO(profileDTO); + + assertEquals(profileDTO.id(), profile.getId()); + assertEquals(profileDTO.name(), profile.getName()); + assertEquals(profileDTO.pgpIdentifier(), String.valueOf(profile.getPgpIdentifier())); + assertArrayEquals(profileDTO.pgpFingerprint(), profile.getProfileFingerprint().getBytes()); + assertArrayEquals(profileDTO.pgpPublicKeyData(), profile.getPgpPublicKeyData()); + assertEquals(profileDTO.accepted(), profile.isAccepted()); + assertEquals(profileDTO.trust(), profile.getTrust()); + } +} diff --git a/app/src/test/java/io/xeres/app/database/repository/ChatRoomRepositoryTest.java b/app/src/test/java/io/xeres/app/database/repository/ChatRoomRepositoryTest.java new file mode 100644 index 000000000..d1747a8ad --- /dev/null +++ b/app/src/test/java/io/xeres/app/database/repository/ChatRoomRepositoryTest.java @@ -0,0 +1,81 @@ +/* + * Copyright (c) 2019-2021 by David Gerber - https://zapek.com + * + * This file is part of Xeres. + * + * Xeres is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Xeres is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Xeres. If not, see . + */ + +package io.xeres.app.database.repository; + +import io.xeres.app.database.model.chatroom.ChatRoom; +import io.xeres.app.database.model.chatroom.ChatRoomFakes; +import io.xeres.app.database.model.identity.IdentityFakes; +import io.xeres.common.identity.Type; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest; + +import java.util.List; +import java.util.Optional; + +import static org.junit.jupiter.api.Assertions.*; + +@DataJpaTest +class ChatRoomRepositoryTest +{ + @Autowired + private ChatRoomRepository chatRoomRepository; + + @Test + void ChatRoomRepository_CRUD_OK() + { + var identity = IdentityFakes.createOwnIdentity("test", Type.SIGNED); + + var chatRoom1 = ChatRoomFakes.createChatRoom(identity); + var chatRoom2 = ChatRoomFakes.createChatRoom(identity); + var chatRoom3 = ChatRoomFakes.createChatRoom(identity); + + chatRoom1.setSubscribed(true); + chatRoom2.setSubscribed(true); + chatRoom3.setSubscribed(false); + + ChatRoom savedChatRoom1 = chatRoomRepository.save(chatRoom1); + chatRoomRepository.save(chatRoom2); + chatRoomRepository.save(chatRoom3); + + List chatRooms = chatRoomRepository.findAllBySubscribedTrueAndJoinedFalse(); + assertNotNull(chatRooms); + assertEquals(2, chatRooms.size()); + + ChatRoom first = chatRoomRepository.findByRoomIdAndIdentity(chatRoom1.getRoomId(), identity).orElse(null); + + assertNotNull(first); + assertEquals(savedChatRoom1.getId(), first.getId()); + assertEquals(savedChatRoom1.getName(), first.getName()); + + first.setJoined(true); + + ChatRoom updatedChatRoom = chatRoomRepository.save(first); + + assertNotNull(updatedChatRoom); + assertEquals(first.getId(), updatedChatRoom.getId()); + assertTrue(updatedChatRoom.isJoined()); + + chatRoomRepository.deleteById(first.getId()); + + Optional deleted = chatRoomRepository.findById(first.getId()); + assertTrue(deleted.isEmpty()); + } +} diff --git a/app/src/test/java/io/xeres/app/database/repository/GxsIdRepositoryTest.java b/app/src/test/java/io/xeres/app/database/repository/GxsIdRepositoryTest.java new file mode 100644 index 000000000..eff134fd4 --- /dev/null +++ b/app/src/test/java/io/xeres/app/database/repository/GxsIdRepositoryTest.java @@ -0,0 +1,76 @@ +/* + * Copyright (c) 2019-2021 by David Gerber - https://zapek.com + * + * This file is part of Xeres. + * + * Xeres is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Xeres is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Xeres. If not, see . + */ + +package io.xeres.app.database.repository; + +import io.xeres.app.database.model.gxs.GxsIdGroupItemFakes; +import io.xeres.app.xrs.service.gxsid.item.GxsIdGroupItem; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest; + +import java.util.List; + +import static org.junit.jupiter.api.Assertions.*; + +@DataJpaTest +class GxsIdRepositoryTest +{ + @Autowired + private GxsIdRepository gxsIdRepository; + + @Test + void GxsIdRepository_CRUD_OK() + { + var gxsIdGroupItem1 = GxsIdGroupItemFakes.createGxsIdGroupItem(); + var gxsIdGroupItem2 = GxsIdGroupItemFakes.createGxsIdGroupItem(); + var gxsIdGroupItem3 = GxsIdGroupItemFakes.createGxsIdGroupItem(); + + var savedGxsIdGroupItem1 = gxsIdRepository.save(gxsIdGroupItem1); + var savedGxsIdGroupItem2 = gxsIdRepository.save(gxsIdGroupItem2); + gxsIdRepository.save(gxsIdGroupItem3); + + List gxsIdGroupItems = gxsIdRepository.findAll(); + assertNotNull(gxsIdGroupItems); + assertEquals(3, gxsIdGroupItems.size()); + + var first = gxsIdRepository.findById(gxsIdGroupItems.get(0).getId()).orElse(null); + assertNotNull(first); + assertEquals(savedGxsIdGroupItem1.getId(), first.getId()); + assertEquals(savedGxsIdGroupItem1.getName(), first.getName()); + + var second = gxsIdRepository.findByGxsId(gxsIdGroupItem2.getGxsId()).orElse(null); + assertNotNull(second); + assertEquals(savedGxsIdGroupItem2.getId(), second.getId()); + assertEquals(savedGxsIdGroupItem2.getName(), second.getName()); + + first.setStatus(1); + + var updateGxsIdGroupItem = gxsIdRepository.save(first); + + assertNotNull(updateGxsIdGroupItem); + assertEquals(first.getId(), updateGxsIdGroupItem.getId()); + assertEquals(1, updateGxsIdGroupItem.getStatus()); + + gxsIdRepository.deleteById(first.getId()); + + var deleted = gxsIdRepository.findById(first.getId()); + assertTrue(deleted.isEmpty()); + } +} diff --git a/app/src/test/java/io/xeres/app/database/repository/LocationRepositoryTest.java b/app/src/test/java/io/xeres/app/database/repository/LocationRepositoryTest.java new file mode 100644 index 000000000..3f0492120 --- /dev/null +++ b/app/src/test/java/io/xeres/app/database/repository/LocationRepositoryTest.java @@ -0,0 +1,77 @@ +/* + * Copyright (c) 2019-2021 by David Gerber - https://zapek.com + * + * This file is part of Xeres. + * + * Xeres is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Xeres is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Xeres. If not, see . + */ + +package io.xeres.app.database.repository; + +import io.xeres.app.database.model.location.Location; +import io.xeres.app.database.model.location.LocationFakes; +import io.xeres.app.database.model.profile.Profile; +import io.xeres.app.database.model.profile.ProfileFakes; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest; + +import java.util.List; +import java.util.Optional; + +import static org.junit.jupiter.api.Assertions.*; + +@DataJpaTest +class LocationRepositoryTest +{ + @Autowired + private LocationRepository locationRepository; + + @Test + void LocationRepository_CRUD_OK() + { + Profile profile = ProfileFakes.createProfile("test", 1); + + Location location1 = LocationFakes.createLocation("test1", profile); + Location location2 = LocationFakes.createLocation("test2", profile); + Location location3 = LocationFakes.createLocation("test3", profile); + + Location savedLocation1 = locationRepository.save(location1); + locationRepository.save(location2); + locationRepository.save(location3); + + List locations = locationRepository.findAll(); + assertNotNull(locations); + assertEquals(3, locations.size()); + + Location first = locationRepository.findById(locations.get(0).getId()).orElse(null); + + assertNotNull(first); + assertEquals(savedLocation1.getId(), first.getId()); + assertEquals(savedLocation1.getName(), first.getName()); + + first.setConnected(true); + + Location updatedLocation = locationRepository.save(first); + + assertNotNull(updatedLocation); + assertEquals(first.getId(), updatedLocation.getId()); + assertTrue(updatedLocation.isConnected()); + + locationRepository.deleteById(first.getId()); + + Optional deleted = locationRepository.findById(first.getId()); + assertTrue(deleted.isEmpty()); + } +} diff --git a/app/src/test/java/io/xeres/app/database/repository/PrefsRepositoryTest.java b/app/src/test/java/io/xeres/app/database/repository/PrefsRepositoryTest.java new file mode 100644 index 000000000..484fe5dc6 --- /dev/null +++ b/app/src/test/java/io/xeres/app/database/repository/PrefsRepositoryTest.java @@ -0,0 +1,76 @@ +/* + * Copyright (c) 2019-2021 by David Gerber - https://zapek.com + * + * This file is part of Xeres. + * + * Xeres is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Xeres is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Xeres. If not, see . + */ + +package io.xeres.app.database.repository; + +import io.xeres.app.database.model.prefs.Prefs; +import io.xeres.app.database.model.prefs.PrefsFakes; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest; + +import java.util.List; +import java.util.Optional; + +import static org.junit.jupiter.api.Assertions.*; + +@DataJpaTest +class PrefsRepositoryTest +{ + @Autowired + private PrefsRepository prefsRepository; + + @Test + void PrefsRepository_CRUD_OK() + { + Prefs prefs = PrefsFakes.createPrefs(); + Prefs unwantedPrefs = PrefsFakes.createPrefs(); + + Prefs savedPrefs = prefsRepository.save(prefs); + prefsRepository.save(unwantedPrefs); + + List prefsList = prefsRepository.findAll(); + assertNotNull(prefsList); + assertEquals(1, prefsList.size()); + + Prefs first = prefsRepository.findById((byte) 1).orElse(null); + + assertNotNull(first); + assertArrayEquals(savedPrefs.getPgpPrivateKeyData(), first.getPgpPrivateKeyData()); + + first.setPgpPrivateKeyData(new byte[]{1}); + + Prefs updatedPrefs = prefsRepository.save(first); + + assertNotNull(updatedPrefs); + assertArrayEquals(first.getPgpPrivateKeyData(), updatedPrefs.getPgpPrivateKeyData()); + + prefsRepository.deleteById((byte) 1); + + Optional deleted = prefsRepository.findById((byte) 1); + assertTrue(deleted.isEmpty()); + + // And then save again to make sure the ID stays at 1 + prefsRepository.save(prefs); + + prefsList = prefsRepository.findAll(); + assertNotNull(prefsList); + assertEquals(1, prefsList.size()); + } +} diff --git a/app/src/test/java/io/xeres/app/database/repository/ProfileRepositoryTest.java b/app/src/test/java/io/xeres/app/database/repository/ProfileRepositoryTest.java new file mode 100644 index 000000000..8f98c2773 --- /dev/null +++ b/app/src/test/java/io/xeres/app/database/repository/ProfileRepositoryTest.java @@ -0,0 +1,72 @@ +/* + * Copyright (c) 2019-2021 by David Gerber - https://zapek.com + * + * This file is part of Xeres. + * + * Xeres is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Xeres is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Xeres. If not, see . + */ + +package io.xeres.app.database.repository; + +import io.xeres.app.database.model.profile.Profile; +import io.xeres.app.database.model.profile.ProfileFakes; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest; + +import java.util.List; + +import static org.junit.jupiter.api.Assertions.*; + +@DataJpaTest +class ProfileRepositoryTest +{ + @Autowired + private ProfileRepository profileRepository; + + @Test + void ProfileRepository_CRUD_OK() + { + var profile1 = ProfileFakes.createProfile("test1", 1); + var profile2 = ProfileFakes.createProfile("test2", 2); + var profile3 = ProfileFakes.createProfile("test3", 3); + + var savedProfile = profileRepository.save(profile1); + profileRepository.save(profile2); + profileRepository.save(profile3); + + List profiles = profileRepository.findAll(); + assertNotNull(profiles); + assertEquals(3, profiles.size()); + + var first = profileRepository.findById(profiles.get(0).getId()).orElse(null); + + assertNotNull(first); + assertEquals(savedProfile.getId(), first.getId()); + assertEquals(savedProfile.getName(), first.getName()); + + first.setAccepted(false); + + var updatedProfile = profileRepository.save(first); + + assertNotNull(updatedProfile); + assertEquals(first.getId(), updatedProfile.getId()); + assertFalse(updatedProfile.isAccepted()); + + profileRepository.deleteById(first.getId()); + + var deleted = profileRepository.findById(first.getId()); + assertTrue(deleted.isEmpty()); + } +} diff --git a/app/src/test/java/io/xeres/app/environment/CloudTest.java b/app/src/test/java/io/xeres/app/environment/CloudTest.java new file mode 100644 index 000000000..fea9e0536 --- /dev/null +++ b/app/src/test/java/io/xeres/app/environment/CloudTest.java @@ -0,0 +1,33 @@ +/* + * Copyright (c) 2019-2021 by David Gerber - https://zapek.com + * + * This file is part of Xeres. + * + * Xeres is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Xeres is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Xeres. If not, see . + */ + +package io.xeres.app.environment; + +import io.xeres.app.application.environment.Cloud; +import io.xeres.testutils.TestUtils; +import org.junit.jupiter.api.Test; + +class CloudTest +{ + @Test + void Cloud_NoInstance_OK() throws NoSuchMethodException + { + TestUtils.assertUtilityClass(Cloud.class); + } +} diff --git a/app/src/test/java/io/xeres/app/environment/CommandArgumentTest.java b/app/src/test/java/io/xeres/app/environment/CommandArgumentTest.java new file mode 100644 index 000000000..2d812828d --- /dev/null +++ b/app/src/test/java/io/xeres/app/environment/CommandArgumentTest.java @@ -0,0 +1,33 @@ +/* + * Copyright (c) 2019-2021 by David Gerber - https://zapek.com + * + * This file is part of Xeres. + * + * Xeres is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Xeres is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Xeres. If not, see . + */ + +package io.xeres.app.environment; + +import io.xeres.app.application.environment.CommandArgument; +import io.xeres.testutils.TestUtils; +import org.junit.jupiter.api.Test; + +class CommandArgumentTest +{ + @Test + void CommandArgument_NoInstance_OK() throws NoSuchMethodException + { + TestUtils.assertUtilityClass(CommandArgument.class); + } +} diff --git a/app/src/test/java/io/xeres/app/environment/HostVariableTest.java b/app/src/test/java/io/xeres/app/environment/HostVariableTest.java new file mode 100644 index 000000000..d924908ba --- /dev/null +++ b/app/src/test/java/io/xeres/app/environment/HostVariableTest.java @@ -0,0 +1,33 @@ +/* + * Copyright (c) 2019-2021 by David Gerber - https://zapek.com + * + * This file is part of Xeres. + * + * Xeres is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Xeres is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Xeres. If not, see . + */ + +package io.xeres.app.environment; + +import io.xeres.app.application.environment.HostVariable; +import io.xeres.testutils.TestUtils; +import org.junit.jupiter.api.Test; + +class HostVariableTest +{ + @Test + void HostVariable_NoInstance_OK() throws NoSuchMethodException + { + TestUtils.assertUtilityClass(HostVariable.class); + } +} diff --git a/app/src/test/java/io/xeres/app/net/bdisc/BroadcastDiscoveryServiceTest.java b/app/src/test/java/io/xeres/app/net/bdisc/BroadcastDiscoveryServiceTest.java new file mode 100644 index 000000000..58bb82501 --- /dev/null +++ b/app/src/test/java/io/xeres/app/net/bdisc/BroadcastDiscoveryServiceTest.java @@ -0,0 +1,70 @@ +/* + * Copyright (c) 2019-2021 by David Gerber - https://zapek.com + * + * This file is part of Xeres. + * + * Xeres is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Xeres is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Xeres. If not, see . + */ + +package io.xeres.app.net.bdisc; + +import io.xeres.app.database.DatabaseSessionManager; +import io.xeres.app.database.model.location.LocationFakes; +import io.xeres.app.service.LocationService; +import io.xeres.common.id.LocationId; +import io.xeres.common.protocol.ip.IP; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.springframework.test.context.junit.jupiter.SpringExtension; + +import java.util.Optional; +import java.util.concurrent.TimeUnit; + +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +@ExtendWith(SpringExtension.class) +class BroadcastDiscoveryServiceTest +{ + @Mock + private LocationService locationService; + + @Mock + private DatabaseSessionManager databaseSessionManager; + + @InjectMocks + private BroadcastDiscoveryService broadcastDiscoveryService; + + @Test + void BroadcastDiscoveryService_StartStop_OK() throws InterruptedException + { + var ownLocation = LocationFakes.createOwnLocation(); + when(locationService.findOwnLocation()).thenReturn(Optional.of(ownLocation)); + when(locationService.findLocationById(any(LocationId.class))).thenReturn(Optional.empty()); + + broadcastDiscoveryService.start(IP.getLocalIpAddress(), 36406); // nothing should reply in there, hopefully. We can't use localhost because linux has no broadcast in it + TimeUnit.SECONDS.sleep(10); + assertTrue(broadcastDiscoveryService.isRunning()); + + broadcastDiscoveryService.stop(); + broadcastDiscoveryService.waitForTermination(); + assertFalse(broadcastDiscoveryService.isRunning()); + verify(locationService).findOwnLocation(); + } +} diff --git a/app/src/test/java/io/xeres/app/net/bdisc/UdpDiscoveryProtocolTest.java b/app/src/test/java/io/xeres/app/net/bdisc/UdpDiscoveryProtocolTest.java new file mode 100644 index 000000000..ee84928f0 --- /dev/null +++ b/app/src/test/java/io/xeres/app/net/bdisc/UdpDiscoveryProtocolTest.java @@ -0,0 +1,90 @@ +/* + * Copyright (c) 2019-2021 by David Gerber - https://zapek.com + * + * This file is part of Xeres. + * + * Xeres is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Xeres is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Xeres. If not, see . + */ + +package io.xeres.app.net.bdisc; + +import io.xeres.common.id.Id; +import io.xeres.common.id.LocationId; +import io.xeres.common.id.ProfileFingerprint; +import io.xeres.testutils.TestUtils; +import org.junit.jupiter.api.Test; + +import java.net.InetSocketAddress; +import java.nio.ByteBuffer; + +import static org.junit.jupiter.api.Assertions.assertArrayEquals; +import static org.junit.jupiter.api.Assertions.assertEquals; + +class UdpDiscoveryProtocolTest +{ + private static final int APP_ID = 904571; + private static final int PEER_ID = 1730783293; + private static final int PACKET_INDEX = 32921; + private static final UdpDiscoveryPeer.Status STATUS_PRESENT = UdpDiscoveryPeer.Status.PRESENT; + private static final ProfileFingerprint FINGERPRINT = new ProfileFingerprint(Id.toBytes("54B7C121B73E434539DC3E0BA87461B115390F34")); + private static final LocationId LOCATION_ID = new LocationId(Id.toBytes("ec65a805a3faa6d4b88e7a2ee5a45f33")); + private static final String LOCAL_IP = "127.0.0.1"; + private static final int LOCAL_PORT = 8600; + private static final String PROFILE_NAME = "retroshare.ch"; + + private static final String DATA = "524e36550000000000000dcd7b6729a83d00008099000037000054b7c121b73e434539dc" + + "3e0ba87461b115390f34ec65a805a3faa6d4b88e7a2ee5a45f3321980000000d726574726f73686172652e6368"; + + @Test + void UdpDiscoveryProtocol_NoInstance_OK() throws NoSuchMethodException + { + TestUtils.assertUtilityClass(UdpDiscoveryProtocol.class); + } + + @Test + void UdpDiscoveryProtocol_ParsePacket_OK() + { + UdpDiscoveryPeer peer = UdpDiscoveryProtocol.parsePacket(ByteBuffer.wrap(Id.toBytes(DATA)), new InetSocketAddress(LOCAL_IP, 6666)); + + assertEquals(APP_ID, peer.getAppId()); + assertEquals(PEER_ID, peer.getPeerId()); + assertEquals(PACKET_INDEX, peer.getPacketIndex()); + assertEquals(STATUS_PRESENT, peer.getStatus()); + assertEquals(FINGERPRINT, peer.getFingerprint()); + assertEquals(LOCATION_ID, peer.getLocationId()); + assertEquals(LOCAL_IP, peer.getIpAddress()); + assertEquals(LOCAL_PORT, peer.getLocalPort()); + assertEquals(PROFILE_NAME, peer.getProfileName()); + } + + @Test + void UdpDiscoveryProtocol_CreatePacket_OK() + { + ByteBuffer data = UdpDiscoveryProtocol.createPacket( + 512, + STATUS_PRESENT, + APP_ID, + PEER_ID, + PACKET_INDEX, + FINGERPRINT, + LOCATION_ID, + LOCAL_PORT, + PROFILE_NAME); + + var a = new byte[data.position()]; + data.flip(); + data.get(a); + assertArrayEquals(Id.toBytes(DATA), a); + } +} diff --git a/app/src/test/java/io/xeres/app/net/peer/AbstractPipelineTest.java b/app/src/test/java/io/xeres/app/net/peer/AbstractPipelineTest.java new file mode 100644 index 000000000..92044ca6e --- /dev/null +++ b/app/src/test/java/io/xeres/app/net/peer/AbstractPipelineTest.java @@ -0,0 +1,35 @@ +/* + * Copyright (c) 2019-2020 by David Gerber - https://zapek.com + * + * This file is part of Xeres. + * + * Xeres is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Xeres is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Xeres. If not, see . + */ + +package io.xeres.app.net.peer; + +import io.netty.buffer.ByteBuf; + +public abstract class AbstractPipelineTest +{ + static byte[] getByteBufAsArray(ByteBuf buf) + { + buf.markReaderIndex(); + buf.readerIndex(0); + var out = new byte[buf.writerIndex()]; + buf.readBytes(out); + buf.resetReaderIndex(); + return out; + } +} diff --git a/app/src/test/java/io/xeres/app/net/peer/PacketBuilder.java b/app/src/test/java/io/xeres/app/net/peer/PacketBuilder.java new file mode 100644 index 000000000..fd49a2e23 --- /dev/null +++ b/app/src/test/java/io/xeres/app/net/peer/PacketBuilder.java @@ -0,0 +1,267 @@ +/* + * Copyright (c) 2019-2020 by David Gerber - https://zapek.com + * + * This file is part of Xeres. + * + * Xeres is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Xeres is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Xeres. If not, see . + */ + +package io.xeres.app.net.peer; + +import io.xeres.app.net.peer.packet.Packet; +import io.xeres.app.xrs.item.RawItem; + +import java.util.concurrent.ThreadLocalRandom; + +// and perhaps not use RsPacket at all since it's a simulator class +// do an OldPacketBuilder and a NewPacketBuilder? +@Deprecated(forRemoval = true) +public class PacketBuilder +{ + public static final class Builder + { + private boolean newPacket = true; + private boolean noAlloc; + private int version = 2; + private int service; + private int subPacket; + private RawItem rawItem; + private int dataSize; + private int headerSize = 8; + private int id; + private int flags = 0; + private Integer priority; + private byte[] data; + + private Builder() + { + } + + public Builder setOldPacket() + { + newPacket = false; + return this; + } + + public Builder setVersion(int version) + { + this.version = version; + return this; + } + + public Builder setStart() + { + //flags = Packet.SLICE_FLAG_START; + return this; + } + + public Builder setEnd() + { + //flags = Packet.SLICE_FLAG_END; + return this; + } + + public Builder setMiddle() + { + flags = 0; + return this; + } + + public Builder setNoAlloc() + { + this.noAlloc = true; + return this; + } + + public Builder setItem(RawItem rawItem) + { + this.rawItem = rawItem; + return this; + } + + public Builder setService(int service) + { + this.service = service; + return this; + } + + public Builder setSubPacket(int subPacket) + { + this.subPacket = subPacket; + return this; + } + + public Builder setDataSize(int size) + { + this.dataSize = size; + return this; + } + + public Builder setHeaderSize(int size) + { + this.headerSize = size; + return this; + } + + public Builder setId(int id) + { + this.id = id; + return this; + } + + public Builder setFlags(int flags) + { + this.flags = flags; + return this; + } + + public Builder setData(byte[] data) + { + this.data = data; + return this; + } + + public Builder setRandomData(int size) + { + this.data = new byte[size]; + ThreadLocalRandom.current().nextBytes(data); + return this; + } + + public Builder setPriority(int priority) + { + this.priority = priority; + return this; + } + + public Packet buildPacket() + { + //RsPacket packet = new RsPacket(); + + if (newPacket) + { + //if ((flags & Packet.SLICE_FLAG_START) != 0) + { + //packet.setStart(true); + } + //if ((flags & Packet.SLICE_FLAG_END) != 0) + { + //packet.setEnd(true); + } + //packet.setId(id); + if (priority != null) + { + //packet.setPriority(priority); + } + + if (dataSize > 0) + { + //packet.setData(new byte[dataSize]); + + if (data != null) + { + if (data.length > dataSize) + { + throw new IllegalArgumentException("data > dataSize"); + } + //System.arraycopy(data, 0, packet.getData(), 0, data.length); + } + } + else + { + if (rawItem != null) + { + //data = item.getData(); + } + if (data != null) + { + //packet.setData(data); + dataSize = data.length; + } + } + + var headerAndData = new byte[noAlloc ? 8 : Math.max(8, headerSize + dataSize)]; + headerAndData[0] = Packet.SLICE_PROTOCOL_VERSION_ID_01; + //if (packet.isStart()) + { + //headerAndData[1] |= Packet.SLICE_FLAG_START; + } + //if (packet.isEnd()) + { + //headerAndData[1] |= Packet.SLICE_FLAG_END; + } + //headerAndData[2] = (byte) (packet.getId() >> 24); + //headerAndData[3] = (byte) (packet.getId() >> 16); + //headerAndData[4] = (byte) (packet.getId() >> 8); + //headerAndData[5] = (byte) (packet.getId()); + headerAndData[6] = (byte) (dataSize >> 8); + headerAndData[7] = (byte) (dataSize); + + //if (packet.getData() != null && !noAlloc && data != null) + //{ + // System.arraycopy(data, 0, headerAndData, 8, data.length); + //} + + //packet.setData(headerAndData); + //return packet; + } + else + { + if (rawItem != null) + { + //packet.setData(item.getData()); + //return packet; + } + + if (dataSize == 0) + { + if (data != null) + { + dataSize = data.length; + } + } + + var headerAndData = new byte[noAlloc ? 8 : Math.max(8, headerSize + dataSize)]; + headerAndData[0] = (byte) version; + headerAndData[1] = (byte) (service >> 8); + headerAndData[2] = (byte) (service); + headerAndData[3] = (byte) subPacket; + headerAndData[4] = (byte) ((dataSize + headerSize) >> 24); + headerAndData[5] = (byte) ((dataSize + headerSize) >> 16); + headerAndData[6] = (byte) ((dataSize + headerSize) >> 8); + headerAndData[7] = (byte) (dataSize + headerSize); + + if (data != null && !noAlloc) + { + System.arraycopy(data, 0, headerAndData, 8, data.length); + } + + //packet.setData(headerAndData); + //return packet; + } + return null; + } + + public byte[] build() + { + //return buildPacket().getData(); + return new byte[0]; + } + } + + public static Builder builder() + { + return new Builder(); + } +} diff --git a/app/src/test/java/io/xeres/app/net/peer/PacketDecoderPipelineTest.java b/app/src/test/java/io/xeres/app/net/peer/PacketDecoderPipelineTest.java new file mode 100644 index 000000000..6022a675d --- /dev/null +++ b/app/src/test/java/io/xeres/app/net/peer/PacketDecoderPipelineTest.java @@ -0,0 +1,390 @@ +/* + * Copyright (c) 2019-2021 by David Gerber - https://zapek.com + * + * This file is part of Xeres. + * + * Xeres is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Xeres is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Xeres. If not, see . + */ + +package io.xeres.app.net.peer; + +import io.netty.buffer.ByteBuf; +import io.netty.buffer.Unpooled; +import io.netty.channel.embedded.EmbeddedChannel; +import io.netty.handler.codec.DecoderException; +import io.netty.handler.codec.TooLongFrameException; +import io.netty.util.ReferenceCountUtil; +import io.xeres.app.net.peer.packet.MultiPacketBuilder; +import io.xeres.app.net.peer.packet.SimplePacketBuilder; +import io.xeres.app.net.peer.pipeline.ItemDecoder; +import io.xeres.app.net.peer.pipeline.PacketDecoder; +import io.xeres.app.xrs.item.RawItem; +import org.bouncycastle.crypto.Digest; +import org.bouncycastle.crypto.digests.SHA256Digest; +import org.junit.jupiter.api.Test; + +import java.net.ProtocolException; +import java.util.concurrent.ThreadLocalRandom; + +import static io.xeres.app.net.peer.packet.MultiPacket.SLICE_FLAG_END; +import static io.xeres.app.net.peer.packet.MultiPacket.SLICE_FLAG_START; +import static io.xeres.app.net.peer.packet.Packet.OPTIMAL_PACKET_SIZE; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.junit.jupiter.api.Assertions.*; + +class PacketDecoderPipelineTest extends AbstractPipelineTest +{ + @Test + void RsFrameDecoder_NewPacket_OK() + { + var channel = new EmbeddedChannel(new PacketDecoder()); + + byte[] inPacket = MultiPacketBuilder.builder() + .build(); + + channel.writeInbound(Unpooled.wrappedBuffer(inPacket)); + ByteBuf inBuf = channel.readInbound(); + assertArrayEquals(inPacket, getByteBufAsArray(inBuf)); + + ReferenceCountUtil.release(inBuf); + } + + @Test + void RsFrameDecoder_NewPacket_ZeroSize() + { + var channel = new EmbeddedChannel(new PacketDecoder(), new ItemDecoder()); + + byte[] inPacket = MultiPacketBuilder.builder() + .build(); + + channel.writeInbound(Unpooled.wrappedBuffer(inPacket)); + RawItem rawItem = channel.readInbound(); + assertNotNull(rawItem); + + ReferenceCountUtil.release(rawItem); + } + + @Test + void RsFrameDecoder_OldPacket_OK() + { + var channel = new EmbeddedChannel(new PacketDecoder()); + + byte[] inPacket = SimplePacketBuilder.builder() + .build(); + + channel.writeInbound(Unpooled.wrappedBuffer(inPacket)); + ByteBuf inBuf = channel.readInbound(); + assertArrayEquals(inPacket, getByteBufAsArray(inBuf)); + + ReferenceCountUtil.release(inBuf); + } + + @Test + void RsFrameDecoder_OldPacket_TooSmall() + { + var channel = new EmbeddedChannel(new PacketDecoder(), new ItemDecoder()); + + byte[] inPacket = SimplePacketBuilder.builder() + .setHeaderSize(6) + .build(); + + assertThatThrownBy(() -> { + channel.writeInbound(Unpooled.wrappedBuffer(inPacket)); + channel.checkException(); + }).isInstanceOf(DecoderException.class) + .hasCauseInstanceOf(ProtocolException.class) + .hasMessageContaining("Packet size too small"); + } + + /** + * The old packets can be oversized, the new ones can't. + */ + @Test + void RsFrameDecoder_OldPacket_Oversized() + { + var channel = new EmbeddedChannel(new PacketDecoder(), new ItemDecoder()); + + byte[] inPacket = SimplePacketBuilder.builder() + .setHeaderSize(Integer.MAX_VALUE - 8) + .build(); + + assertThatThrownBy(() -> { + channel.writeInbound(Unpooled.wrappedBuffer(inPacket)); + channel.checkException(); + }).isInstanceOf(TooLongFrameException.class) + .hasMessageStartingWith("Frame is too long"); + } + + @Test + void RsPacketDecoder_OldPacket_Empty_OK() + { + var channel = new EmbeddedChannel(new PacketDecoder(), new ItemDecoder()); + + byte[] inPacket = SimplePacketBuilder.builder() + .build(); + + channel.writeInbound(Unpooled.wrappedBuffer(inPacket)); + RawItem rawItem = channel.readInbound(); + assertNotNull(rawItem); + + ReferenceCountUtil.release(rawItem); + } + + @Test + void RsPacketDecoder_NewPacket_Empty_DoubleStartPacket() + { + var channel = new EmbeddedChannel(new PacketDecoder(), new ItemDecoder()); + + byte[] inPacket = MultiPacketBuilder.builder() + .setFlags(SLICE_FLAG_START) + .build(); + + channel.writeInbound(Unpooled.wrappedBuffer(inPacket)); + assertThatThrownBy(() -> { + channel.writeInbound(Unpooled.wrappedBuffer(inPacket)); + channel.checkException(); + }).isInstanceOf(DecoderException.class) + .hasCauseInstanceOf(ProtocolException.class) + .hasMessageFindingMatch("Start packet [0-9]* already received"); + } + + @Test + void RsPacketDecoder_NewPacket_Empty_MiddlePacketWithoutStartPacket() + { + var channel = new EmbeddedChannel(new PacketDecoder(), new ItemDecoder()); + + byte[] inPacket = MultiPacketBuilder.builder() + .setFlags(0) + .build(); + + assertThatThrownBy(() -> { + channel.writeInbound(Unpooled.wrappedBuffer(inPacket)); + channel.checkException(); + }).isInstanceOf(DecoderException.class) + .hasCauseInstanceOf(ProtocolException.class) + .hasMessageFindingMatch("Middle packet [0-9]* received without corresponding start packet"); + } + + @Test + void RsPacketDecoder_NewPacket_Empty_EndPacketWithoutStartPacket() + { + var channel = new EmbeddedChannel(new PacketDecoder(), new ItemDecoder()); + + byte[] inPacket = MultiPacketBuilder.builder() + .setFlags(SLICE_FLAG_END) + .build(); + + assertThatThrownBy(() -> { + channel.writeInbound(Unpooled.wrappedBuffer(inPacket)); + channel.checkException(); + }).isInstanceOf(DecoderException.class) + .hasCauseInstanceOf(ProtocolException.class) + .hasMessageFindingMatch("End packet [0-9]* received without corresponding start packet"); + } + + @Test + void RsPacketDecoder_NewPacket_Empty_OK() + { + var channel = new EmbeddedChannel(new PacketDecoder(), new ItemDecoder()); + + byte[] inPacket = MultiPacketBuilder.builder() + .build(); + + channel.writeInbound(Unpooled.wrappedBuffer(inPacket)); + RawItem rawItem = channel.readInbound(); + assertNotNull(rawItem); + assertEquals(0, rawItem.getBuffer().writerIndex()); + assertFalse(channel.finish()); + + ReferenceCountUtil.release(rawItem); + } + + @Test + void RsPacketDecoder_NewPacket_Slicing_SizesWithHeaders_OK() + { + var channel = new EmbeddedChannel(new PacketDecoder(), new ItemDecoder()); + + byte[] inPacket1 = MultiPacketBuilder.builder() + .setPacketId(1) + .setFlags(SLICE_FLAG_START) + .setData(new byte[OPTIMAL_PACKET_SIZE]) + .build(); + + byte[] inPacket2 = MultiPacketBuilder.builder() + .setPacketId(1) + .setFlags(0) + .setData(new byte[200]) + .build(); + + byte[] inPacket3 = MultiPacketBuilder.builder() + .setPacketId(1) + .setFlags(SLICE_FLAG_END) + .setData(new byte[100]) + .build(); + + channel.writeInbound(Unpooled.wrappedBuffer(inPacket1)); + channel.writeInbound(Unpooled.wrappedBuffer(inPacket2)); + channel.writeInbound(Unpooled.wrappedBuffer(inPacket3)); + + RawItem rawItem = channel.readInbound(); + assertNotNull(rawItem); + assertEquals(OPTIMAL_PACKET_SIZE + 200 + 100, rawItem.getBuffer().writerIndex()); + assertFalse(channel.finish()); + + ReferenceCountUtil.release(rawItem); + } + + /** + * Creates 3 sliced buffers and tests if they're reassembled properly. + */ + @Test + void RsPacketDecoder_NewPacket_Slicing_DataIntegrity_OK() + { + var channel = new EmbeddedChannel(new PacketDecoder(), new ItemDecoder()); + + var data1 = new byte[OPTIMAL_PACKET_SIZE]; + var data2 = new byte[200]; + var data3 = new byte[100]; + + ThreadLocalRandom.current().nextBytes(data1); + ThreadLocalRandom.current().nextBytes(data2); + ThreadLocalRandom.current().nextBytes(data3); + + byte[] hashIn = computeHash(data1, data2, data3); + + byte[] inPacket1 = MultiPacketBuilder.builder() + .setPacketId(1) + .setFlags(SLICE_FLAG_START) + .setData(data1) + .build(); + + byte[] inPacket2 = MultiPacketBuilder.builder() + .setPacketId(1) + .setFlags(0) + .setData(data2) + .build(); + + byte[] inPacket3 = MultiPacketBuilder.builder() + .setPacketId(1) + .setFlags(SLICE_FLAG_END) + .setData(data3) + .build(); + + channel.writeInbound(Unpooled.wrappedBuffer(inPacket1)); + channel.writeInbound(Unpooled.wrappedBuffer(inPacket2)); + channel.writeInbound(Unpooled.wrappedBuffer(inPacket3)); + + RawItem rawItem = channel.readInbound(); + assertNotNull(rawItem); + assertEquals(data1.length + data2.length + data3.length, rawItem.getBuffer().writerIndex()); + assertFalse(channel.finish()); + assertArrayEquals(hashIn, computeHash(getByteBufAsArray(rawItem.getBuffer()))); + + ReferenceCountUtil.release(rawItem); + } + + @Test + void RsPacketDecoder_NewPacket_Slicing_DataIntegrity_Intermixed_OK() + { + var channel = new EmbeddedChannel(new PacketDecoder(), new ItemDecoder()); + + var dataA1 = new byte[100]; + var dataA2 = new byte[150]; + var dataA3 = new byte[200]; + + var dataB1 = new byte[300]; + var dataB2 = new byte[200]; + var dataB3 = new byte[100]; + + ThreadLocalRandom.current().nextBytes(dataA1); + ThreadLocalRandom.current().nextBytes(dataA2); + ThreadLocalRandom.current().nextBytes(dataA3); + + ThreadLocalRandom.current().nextBytes(dataB1); + ThreadLocalRandom.current().nextBytes(dataB2); + ThreadLocalRandom.current().nextBytes(dataB3); + + byte[] hashInA = computeHash(dataA1, dataA2, dataA3); + byte[] hashInB = computeHash(dataB1, dataB2, dataB3); + + byte[] inPacketA1 = MultiPacketBuilder.builder() + .setPacketId(1) + .setFlags(SLICE_FLAG_START) + .setData(dataA1) + .build(); + + byte[] inPacketA2 = MultiPacketBuilder.builder() + .setPacketId(1) + .setFlags(0) + .setData(dataA2) + .build(); + + byte[] inPacketA3 = MultiPacketBuilder.builder() + .setPacketId(1) + .setFlags(SLICE_FLAG_END) + .setData(dataA3) + .build(); + + byte[] inPacketB1 = MultiPacketBuilder.builder() + .setPacketId(2) + .setFlags(SLICE_FLAG_START) + .setData(dataB1) + .build(); + + byte[] inPacketB2 = MultiPacketBuilder.builder() + .setPacketId(2) + .setFlags(0) + .setData(dataB2) + .build(); + + byte[] inPacketB3 = MultiPacketBuilder.builder() + .setPacketId(2) + .setFlags(SLICE_FLAG_END) + .setData(dataB3) + .build(); + + channel.writeInbound(Unpooled.wrappedBuffer(inPacketA1)); + channel.writeInbound(Unpooled.wrappedBuffer(inPacketB1)); + channel.writeInbound(Unpooled.wrappedBuffer(inPacketA2)); + channel.writeInbound(Unpooled.wrappedBuffer(inPacketB2)); + channel.writeInbound(Unpooled.wrappedBuffer(inPacketA3)); + channel.writeInbound(Unpooled.wrappedBuffer(inPacketB3)); + + RawItem rawItemA = channel.readInbound(); + RawItem rawItemB = channel.readInbound(); + assertNotNull(rawItemA); + assertNotNull(rawItemB); + assertEquals(dataA1.length + dataA2.length + dataA3.length, rawItemA.getBuffer().writerIndex()); + assertEquals(dataB1.length + dataB2.length + dataB3.length, rawItemB.getBuffer().writerIndex()); + assertFalse(channel.finish()); + assertArrayEquals(hashInA, computeHash(getByteBufAsArray(rawItemA.getBuffer()))); + assertArrayEquals(hashInB, computeHash(getByteBufAsArray(rawItemB.getBuffer()))); + + ReferenceCountUtil.release(rawItemA); + ReferenceCountUtil.release(rawItemB); + } + + private byte[] computeHash(byte[]... buffers) + { + var hash = new byte[32]; + + Digest digest = new SHA256Digest(); + for (byte[] buf : buffers) + { + digest.update(buf, 0, buf.length); + } + digest.doFinal(hash, 0); + return hash; + } +} diff --git a/app/src/test/java/io/xeres/app/net/peer/PacketEncoderPipelineTest.java b/app/src/test/java/io/xeres/app/net/peer/PacketEncoderPipelineTest.java new file mode 100644 index 000000000..abc4da99b --- /dev/null +++ b/app/src/test/java/io/xeres/app/net/peer/PacketEncoderPipelineTest.java @@ -0,0 +1,215 @@ +/* + * Copyright (c) 2019-2021 by David Gerber - https://zapek.com + * + * This file is part of Xeres. + * + * Xeres is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Xeres is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Xeres. If not, see . + */ + +package io.xeres.app.net.peer; + +import io.netty.buffer.ByteBuf; +import io.netty.channel.embedded.EmbeddedChannel; +import io.netty.util.ReferenceCountUtil; +import io.xeres.app.net.peer.packet.Packet; +import io.xeres.app.net.peer.packet.SimplePacketBuilder; +import io.xeres.app.net.peer.pipeline.SimplePacketEncoder; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +class PacketEncoderPipelineTest extends AbstractPipelineTest +{ + @Test + void RsOldPacketEncoder_OK() + { + var channel = new EmbeddedChannel(new SimplePacketEncoder()); + + Packet outPacket = SimplePacketBuilder.builder().buildPacket(); + + channel.writeAndFlush(outPacket); + ByteBuf outBuf = channel.readOutbound(); + assertEquals(outPacket.getSize(), outBuf.writerIndex()); + ReferenceCountUtil.release(outBuf); + } + + public void RsNewPacketEncoder_OK() + { +// var channel = new EmbeddedChannel(new MultiPacketEncoder()); +// +// Packet inPacket = PacketBuilder.builder().buildPacket(); +// +// channel.attr(PeerHandler.MULTI_PACKET).set(true); +// channel.writeAndFlush(inPacket); +// ByteBuf outBuf = channel.readOutbound(); +// assertEquals(inPacket.getSize(), outBuf.readableBytes()); +// ReferenceCountUtil.release(outBuf); + } + + public void RsNewPacketEncoder_OldPacket_OK() + { +// var channel = new EmbeddedChannel(new MultiPacketEncoder()); +// +// Packet inPacket = PacketBuilder.builder() +// .setData(new byte[]{1, 2, 3, 4}) +// .buildPacket(); +// +// channel.writeAndFlush(inPacket); +// var outPacket = new byte[4]; +// ByteBuf outBuf = channel.readOutbound(); +// outBuf.readBytes(outPacket); +// //assertArrayEquals(inPacket.getData(), outPacket); +// +// ReferenceCountUtil.release(outBuf); + } + + public void RsPacketEncoder_Small_OK() + { +// var channel = new EmbeddedChannel(new MultiPacketEncoder()); +// +// Packet inPacket = PacketBuilder.builder() +// .setData(new byte[]{1, 2, 3, 4}) +// .buildPacket(); +// +// channel.attr(PeerHandler.MULTI_PACKET).set(true); +// channel.writeAndFlush(inPacket); +// channel.runPendingTasks(); +// var outPacket = new byte[4]; +// skipHeader(channel); +// ByteBuf outBuf = channel.readOutbound(); +// outBuf.readBytes(outPacket); +// //assertArrayEquals(inPacket.getData(), outPacket); +// +// ReferenceCountUtil.release(outBuf); + } + + public void RsPacketEncoder_Optimal_OK() + { +// EmbeddedChannel channel = new EmbeddedChannel(new MultiPacketEncoder()); +// +// Packet inPacket = PacketBuilder.builder() +// .setRandomData(Packet.OPTIMAL_PACKET_SIZE) +// .buildPacket(); +// +// channel.attr(PeerHandler.MULTI_PACKET).set(true); +// channel.writeAndFlush(inPacket); +// channel.runPendingTasks(); +// var outPacket = new byte[Packet.OPTIMAL_PACKET_SIZE]; +// skipHeader(channel); +// ByteBuf outBuf = channel.readOutbound(); +// outBuf.readBytes(outPacket); +// //assertArrayEquals(inPacket.getData(), outPacket); +// +// ReferenceCountUtil.release(outBuf); + } + + public void RsPacketEncoder_Big_OK() + { +// var channel = new EmbeddedChannel(new MultiPacketEncoder()); +// +// byte[] inPacket = PacketBuilder.builder() +// .setRandomData(Packet.OPTIMAL_PACKET_SIZE * 3 + 200) +// .build(); +// +// channel.attr(PeerHandler.MULTI_PACKET).set(true); +// channel.writeAndFlush(Unpooled.wrappedBuffer(inPacket)); +// ByteBuf outBuf = channel.readOutbound(); +// byte[] outPacket = outBuf.array(); +// assertArrayEquals(inPacket, outPacket); +// +// ReferenceCountUtil.release(outBuf); + } + + public void RsPacketEncoder_Multiple_OK() + { +// var channel = new EmbeddedChannel(new MultiPacketEncoder()); +// +// byte[] inPacket1 = PacketBuilder.builder() +// .setRandomData(Packet.OPTIMAL_PACKET_SIZE * 3 + 200) +// .build(); +// +// byte[] inPacket2 = PacketBuilder.builder() +// .setRandomData(6) +// .build(); +// +// byte[] inPacket3 = PacketBuilder.builder() +// .setRandomData(Packet.OPTIMAL_PACKET_SIZE * 2) +// .build(); +// +// channel.attr(PeerHandler.MULTI_PACKET).set(true); +// channel.writeAndFlush(Unpooled.wrappedBuffer(inPacket1)); +// channel.writeAndFlush(Unpooled.wrappedBuffer(inPacket2)); +// channel.writeAndFlush(Unpooled.wrappedBuffer(inPacket3)); +// ByteBuf outBuf1 = channel.readOutbound(); +// byte[] outPacket1 = outBuf1.array(); +// ByteBuf outBuf2 = channel.readOutbound(); +// byte[] outPacket2 = outBuf1.array(); +// ByteBuf outBuf3 = channel.readOutbound(); +// byte[] outPacket3 = outBuf1.array(); +// +// assertArrayEquals(inPacket1, outPacket1); +// assertArrayEquals(inPacket2, outPacket2); +// assertArrayEquals(inPacket3, outPacket3); +// +// ReferenceCountUtil.release(outBuf1); +// ReferenceCountUtil.release(outBuf2); +// ReferenceCountUtil.release(outBuf3); + } + + public void RsPacketEncoder_Multiple_Priority_OK() + { +// var channel = new EmbeddedChannel(new MultiPacketEncoder()); +// +// byte[] inPacket1 = PacketBuilder.builder() +// .setRandomData(Packet.OPTIMAL_PACKET_SIZE * 3 + 200) +// .build(); +// +// byte[] inPacket2 = PacketBuilder.builder() +// .setRandomData(6) +// .setPriority(9) +// .build(); +// +// byte[] inPacket3 = PacketBuilder.builder() +// .setRandomData(Packet.OPTIMAL_PACKET_SIZE * 2) +// .build(); +// +// channel.attr(PeerHandler.MULTI_PACKET).set(true); +// channel.writeAndFlush(Unpooled.wrappedBuffer(inPacket1)); +// channel.writeAndFlush(Unpooled.wrappedBuffer(inPacket2)); +// channel.writeAndFlush(Unpooled.wrappedBuffer(inPacket3)); +// +// ByteBuf outBuf1 = channel.readOutbound(); +// byte[] outPacket1 = outBuf1.array(); +// ByteBuf outBuf2 = channel.readOutbound(); +// byte[] outPacket2 = outBuf1.array(); +// ByteBuf outBuf3 = channel.readOutbound(); +// byte[] outPacket3 = outBuf1.array(); +// +// assertArrayEquals(inPacket1, outPacket1); +// assertArrayEquals(inPacket2, outPacket2); +// assertArrayEquals(inPacket3, outPacket3); +// +// ReferenceCountUtil.release(outBuf1); +// ReferenceCountUtil.release(outBuf2); +// ReferenceCountUtil.release(outBuf3); + } + + private void skipHeader(EmbeddedChannel channel) + { + ByteBuf byteBuf = channel.readOutbound(); + byteBuf.skipBytes(8); + + ReferenceCountUtil.release(byteBuf); + } +} diff --git a/app/src/test/java/io/xeres/app/net/peer/PeerAttributeTest.java b/app/src/test/java/io/xeres/app/net/peer/PeerAttributeTest.java new file mode 100644 index 000000000..154bfa1c2 --- /dev/null +++ b/app/src/test/java/io/xeres/app/net/peer/PeerAttributeTest.java @@ -0,0 +1,32 @@ +/* + * Copyright (c) 2019-2021 by David Gerber - https://zapek.com + * + * This file is part of Xeres. + * + * Xeres is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Xeres is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Xeres. If not, see . + */ + +package io.xeres.app.net.peer; + +import io.xeres.testutils.TestUtils; +import org.junit.jupiter.api.Test; + +class PeerAttributeTest +{ + @Test + void PeerAttribute_NoInstance_OK() throws NoSuchMethodException + { + TestUtils.assertUtilityClass(PeerAttribute.class); + } +} diff --git a/app/src/test/java/io/xeres/app/net/peer/RawItemDecoderPipelineTest.java b/app/src/test/java/io/xeres/app/net/peer/RawItemDecoderPipelineTest.java new file mode 100644 index 000000000..f42bbf8d2 --- /dev/null +++ b/app/src/test/java/io/xeres/app/net/peer/RawItemDecoderPipelineTest.java @@ -0,0 +1,86 @@ +/* + * Copyright (c) 2019-2021 by David Gerber - https://zapek.com + * + * This file is part of Xeres. + * + * Xeres is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Xeres is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Xeres. If not, see . + */ + +package io.xeres.app.net.peer; + +import io.netty.buffer.ByteBufAllocator; +import io.netty.buffer.Unpooled; +import io.netty.channel.embedded.EmbeddedChannel; +import io.netty.util.ReferenceCountUtil; +import io.xeres.app.net.peer.packet.MultiPacketBuilder; +import io.xeres.app.net.peer.packet.SimplePacketBuilder; +import io.xeres.app.net.peer.pipeline.ItemDecoder; +import io.xeres.app.net.peer.pipeline.PacketDecoder; +import io.xeres.app.xrs.item.RawItem; +import io.xeres.app.xrs.serialization.SerializationFlags; +import io.xeres.app.xrs.service.RsServiceType; +import io.xeres.app.xrs.service.sliceprobe.item.SliceProbeItem; +import org.junit.jupiter.api.Test; + +import java.util.EnumSet; + +import static org.junit.jupiter.api.Assertions.assertArrayEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; + +class RawItemDecoderPipelineTest extends AbstractPipelineTest +{ + @Test + void RsItemDecoder_NewPacket_Decode_OK() + { + var channel = new EmbeddedChannel(new PacketDecoder(), new ItemDecoder()); + + var item = new SliceProbeItem(); + item.setOutgoing(ByteBufAllocator.DEFAULT, 2, RsServiceType.PACKET_SLICING_PROBE, 0xaa); + var itemIn = item.serializeItem(EnumSet.noneOf(SerializationFlags.class)); + + byte[] inPacket = MultiPacketBuilder.builder() + .setRawItem(itemIn) + .build(); + + channel.writeInbound(Unpooled.wrappedBuffer(inPacket)); + + RawItem rawItemOut = channel.readInbound(); + assertNotNull(rawItemOut); + assertArrayEquals(getByteBufAsArray(itemIn.getBuffer()), getByteBufAsArray(rawItemOut.getBuffer())); + + ReferenceCountUtil.release(rawItemOut); + } + + @Test + void RsItemDecoder_OldPacket_Decode_OK() + { + var channel = new EmbeddedChannel(new PacketDecoder(), new ItemDecoder()); + + var item = new SliceProbeItem(); + item.setOutgoing(ByteBufAllocator.DEFAULT, 2, RsServiceType.PACKET_SLICING_PROBE, 0xaa); + var itemIn = item.serializeItem(EnumSet.noneOf(SerializationFlags.class)); + + byte[] inPacket = SimplePacketBuilder.builder() + .setRawItem(itemIn) + .build(); + + channel.writeInbound(Unpooled.wrappedBuffer(inPacket)); + + RawItem rawItemOut = channel.readInbound(); + assertNotNull(rawItemOut); + assertArrayEquals(getByteBufAsArray(itemIn.getBuffer()), getByteBufAsArray(rawItemOut.getBuffer())); + + ReferenceCountUtil.release(rawItemOut); + } +} diff --git a/app/src/test/java/io/xeres/app/net/peer/packet/MultiPacketBuilder.java b/app/src/test/java/io/xeres/app/net/peer/packet/MultiPacketBuilder.java new file mode 100644 index 000000000..21f40aa0d --- /dev/null +++ b/app/src/test/java/io/xeres/app/net/peer/packet/MultiPacketBuilder.java @@ -0,0 +1,107 @@ +/* + * Copyright (c) 2019-2020 by David Gerber - https://zapek.com + * + * This file is part of Xeres. + * + * Xeres is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Xeres is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Xeres. If not, see . + */ + +package io.xeres.app.net.peer.packet; + +import io.netty.buffer.ByteBuf; +import io.netty.buffer.Unpooled; +import io.xeres.app.xrs.item.RawItem; + +import static io.xeres.app.net.peer.packet.MultiPacket.SLICE_FLAG_END; +import static io.xeres.app.net.peer.packet.MultiPacket.SLICE_FLAG_START; +import static io.xeres.app.net.peer.packet.Packet.SLICE_PROTOCOL_VERSION_ID_01; + +public final class MultiPacketBuilder +{ + private MultiPacketBuilder() + { + throw new UnsupportedOperationException("Utility class"); + } + + public static final class Builder + { + private int flags = SLICE_FLAG_START | SLICE_FLAG_END; // XXX: make that settable + private int packetId; + private byte[] data = new byte[0]; + private RawItem rawItem; + + private Builder() + { + } + + public Builder setFlags(int flags) + { + this.flags = flags; + return this; + } + + public Builder setPacketId(int packetId) + { + this.packetId = packetId; + return this; + } + + public Builder setData(byte[] data) + { + this.data = data; + return this; + } + + public Builder setRawItem(RawItem rawItem) + { + this.rawItem = rawItem; + return this; + } + + public MultiPacket buildPacket() + { + ByteBuf buf = Unpooled.buffer(); + + buf.writeByte(SLICE_PROTOCOL_VERSION_ID_01); + buf.writeByte(flags); + buf.writeInt(packetId); + + if (rawItem != null) + { + ByteBuf itemBuf = rawItem.getBuffer(); + buf.writeShort(itemBuf.writerIndex()); + buf.writeBytes(rawItem.getBuffer()); + } + else + { + buf.writeShort(data.length); + buf.writeBytes(data); + } + return new MultiPacket(buf); + } + + public byte[] build() // XXX: is it what we want or do we just need the content? + { + ByteBuf buf = buildPacket().getBuffer(); + var bytes = new byte[buf.writerIndex()]; + buf.readBytes(bytes); + return bytes; + } + } + + public static Builder builder() + { + return new Builder(); + } +} diff --git a/app/src/test/java/io/xeres/app/net/peer/packet/PacketTest.java b/app/src/test/java/io/xeres/app/net/peer/packet/PacketTest.java new file mode 100644 index 000000000..8b227c015 --- /dev/null +++ b/app/src/test/java/io/xeres/app/net/peer/packet/PacketTest.java @@ -0,0 +1,42 @@ +/* + * Copyright (c) 2019-2021 by David Gerber - https://zapek.com + * + * This file is part of Xeres. + * + * Xeres is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Xeres is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Xeres. If not, see . + */ + +package io.xeres.app.net.peer.packet; + +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +class PacketTest +{ + @Test + void Packet_IsSimple_OK() + { + var packet = SimplePacketBuilder.builder().buildPacket(); + assertFalse(packet.isMulti()); + } + + @Test + void Packet_IsMulti_OK() + { + var packet = MultiPacketBuilder.builder().buildPacket(); + assertTrue(packet.isMulti()); + } +} diff --git a/app/src/test/java/io/xeres/app/net/peer/packet/SimplePacketBuilder.java b/app/src/test/java/io/xeres/app/net/peer/packet/SimplePacketBuilder.java new file mode 100644 index 000000000..7bf4d8365 --- /dev/null +++ b/app/src/test/java/io/xeres/app/net/peer/packet/SimplePacketBuilder.java @@ -0,0 +1,116 @@ +/* + * Copyright (c) 2019-2020 by David Gerber - https://zapek.com + * + * This file is part of Xeres. + * + * Xeres is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Xeres is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Xeres. If not, see . + */ + +package io.xeres.app.net.peer.packet; + +import io.netty.buffer.ByteBuf; +import io.netty.buffer.Unpooled; +import io.xeres.app.xrs.item.RawItem; + +import static io.xeres.app.net.peer.packet.Packet.HEADER_SIZE; + +public final class SimplePacketBuilder +{ + private SimplePacketBuilder() + { + throw new UnsupportedOperationException("Utility class"); + } + + public static final class Builder + { + private int headerSize = HEADER_SIZE; + private int version; + private int service; + private int subPacket; + private byte[] data = new byte[0]; + private RawItem rawItem; + + private Builder() + { + } + + public Builder setVersion(int version) + { + this.version = version; + return this; + } + + public Builder setService(int service) + { + this.service = service; + return this; + } + + public Builder setSubPacket(int subPacket) + { + this.subPacket = subPacket; + return this; + } + + public Builder setData(byte[] data) + { + this.data = data; + return this; + } + + public Builder setRawItem(RawItem rawItem) + { + this.rawItem = rawItem; + return this; + } + + public Builder setHeaderSize(int size) + { + this.headerSize = size; + return this; + } + + public SimplePacket buildPacket() + { + ByteBuf buf = Unpooled.buffer(); + + if (rawItem != null) + { + buf.writeBytes(rawItem.getBuffer()); + } + else + { + buf.writeByte(version); + buf.writeShort(service); + buf.writeByte(subPacket); + buf.writeInt(headerSize + data.length); + buf.writeBytes(data); + } + return new SimplePacket(buf); + } + + public byte[] build() + { + ByteBuf buf = buildPacket().getBuffer(); + var bytes = new byte[buf.writerIndex()]; + buf.readBytes(bytes); + return bytes; + } + } + + public static Builder builder() + { + return new Builder(); + } +} diff --git a/app/src/test/java/io/xeres/app/net/peer/ssl/SSLTest.java b/app/src/test/java/io/xeres/app/net/peer/ssl/SSLTest.java new file mode 100644 index 000000000..e4c506963 --- /dev/null +++ b/app/src/test/java/io/xeres/app/net/peer/ssl/SSLTest.java @@ -0,0 +1,159 @@ +/* + * Copyright (c) 2019-2021 by David Gerber - https://zapek.com + * + * This file is part of Xeres. + * + * Xeres is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Xeres is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Xeres. If not, see . + */ + +package io.xeres.app.net.peer.ssl; + +import io.netty.handler.ssl.SslContext; +import io.xeres.app.crypto.pgp.PGP; +import io.xeres.app.crypto.rsa.RSA; +import io.xeres.app.crypto.rsid.RSSerialVersion; +import io.xeres.app.crypto.x509.X509; +import io.xeres.app.database.model.location.Location; +import io.xeres.app.database.model.location.LocationFakes; +import io.xeres.app.database.model.profile.ProfileFakes; +import io.xeres.app.service.LocationService; +import io.xeres.common.id.LocationId; +import io.xeres.testutils.TestUtils; +import org.bouncycastle.jce.provider.BouncyCastleProvider; +import org.bouncycastle.openpgp.PGPException; +import org.bouncycastle.openpgp.PGPSecretKey; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.springframework.test.context.junit.jupiter.SpringExtension; + +import javax.net.ssl.SSLException; +import java.io.IOException; +import java.security.KeyPair; +import java.security.NoSuchAlgorithmException; +import java.security.Security; +import java.security.cert.CertificateException; +import java.security.cert.X509Certificate; +import java.security.spec.InvalidKeySpecException; +import java.util.Date; +import java.util.Optional; + +import static io.xeres.app.net.peer.ConnectionDirection.INCOMING; +import static io.xeres.app.net.peer.ConnectionDirection.OUTGOING; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.Mockito.*; + +@ExtendWith(SpringExtension.class) +class SSLTest +{ + private static PGPSecretKey pgpKey; + private static KeyPair rsaKey; + private static X509Certificate certificate; + + @Mock + private LocationService locationService; + + @BeforeAll + static void setup() throws PGPException, IOException, CertificateException + { + Security.addProvider(new BouncyCastleProvider()); + + pgpKey = PGP.generateSecretKey("foo", "", 512); + rsaKey = RSA.generateKeys(512); + certificate = X509.generateCertificate(pgpKey, rsaKey.getPublic(), "CN=me", "CN=foobar", new Date(0), new Date(0), RSSerialVersion.V07_0001.serialNumber()); + } + + @Test + void SSL_NoInstance_OK() throws NoSuchMethodException + { + TestUtils.assertUtilityClass(SSL.class); + } + + @Test + void SSL_CreateClientContext_OK() throws InvalidKeySpecException, NoSuchAlgorithmException, SSLException + { + SslContext sslContext = SSL.createSslContext(rsaKey.getPrivate().getEncoded(), certificate, OUTGOING); + + assertNotNull(sslContext); + assertTrue(sslContext.isClient()); + } + + @Test + void SSL_CreateServerContext_OK() throws InvalidKeySpecException, NoSuchAlgorithmException, SSLException + { + SslContext sslContext = SSL.createSslContext(rsaKey.getPrivate().getEncoded(), certificate, INCOMING); + + assertNotNull(sslContext); + assertTrue(sslContext.isServer()); + } + + @Test + void SSL_CheckPeerCertificate_OK() throws CertificateException, IOException + { + var profile = ProfileFakes.createProfile("foo", pgpKey.getKeyID(), pgpKey.getPublicKey().getFingerprint(), pgpKey.getPublicKey().getEncoded()); + var location = LocationFakes.createLocation("bar", profile); + + when(locationService.findLocationById(any(LocationId.class))).thenReturn(Optional.of(location)); + + Location result = SSL.checkPeerCertificate(locationService, new X509Certificate[]{certificate}); + + assertEquals(result, location); + verify(locationService).findLocationById(any(LocationId.class)); + } + + @Test + void SSL_CheckPeerCertificate_EmptyCertificate_Fail() + { + assertThatThrownBy(() -> SSL.checkPeerCertificate(locationService, new X509Certificate[]{})) + .isInstanceOf(CertificateException.class) + .hasMessage("Empty certificate"); + + verify(locationService, times(0)).findLocationById(any(LocationId.class)); + } + + @Test + void SSL_CheckPeerCertificate_AlreadyConnected_Fail() throws IOException + { + var profile = ProfileFakes.createProfile("foo", pgpKey.getKeyID(), pgpKey.getPublicKey().getFingerprint(), pgpKey.getPublicKey().getEncoded()); + var location = LocationFakes.createLocation("bar", profile); + location.setConnected(true); + + when(locationService.findLocationById(any(LocationId.class))).thenReturn(Optional.of(location)); + + assertThatThrownBy(() -> SSL.checkPeerCertificate(locationService, new X509Certificate[]{certificate})) + .isInstanceOf(CertificateException.class) + .hasMessage("Already connected"); + + verify(locationService).findLocationById(any(LocationId.class)); + } + + @Test + void SSL_CheckPeerCertificate_WrongCertificate_Fail() throws CertificateException, IOException, PGPException + { + PGPSecretKey wrongPgpKey = PGP.generateSecretKey("notFoo", "", 512); + X509Certificate wrongCertificate = X509.generateCertificate(wrongPgpKey, rsaKey.getPublic(), "CN=me", "CN=foobar", new Date(0), new Date(0), RSSerialVersion.V07_0001.serialNumber()); + var profile = ProfileFakes.createProfile("foo", pgpKey.getKeyID(), pgpKey.getPublicKey().getFingerprint(), pgpKey.getPublicKey().getEncoded()); + var location = LocationFakes.createLocation("bar", profile); + + when(locationService.findLocationById(any(LocationId.class))).thenReturn(Optional.of(location)); + + assertThatThrownBy(() -> SSL.checkPeerCertificate(locationService, new X509Certificate[]{wrongCertificate})) + .isInstanceOf(CertificateException.class) + .hasMessageContaining("Wrong signature"); + + verify(locationService).findLocationById(any(LocationId.class)); + } +} diff --git a/app/src/test/java/io/xeres/app/net/protocol/PeerAddressTest.java b/app/src/test/java/io/xeres/app/net/protocol/PeerAddressTest.java new file mode 100644 index 000000000..63a3a9290 --- /dev/null +++ b/app/src/test/java/io/xeres/app/net/protocol/PeerAddressTest.java @@ -0,0 +1,307 @@ +/* + * Copyright (c) 2019-2021 by David Gerber - https://zapek.com + * + * This file is part of Xeres. + * + * Xeres is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Xeres is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Xeres. If not, see . + */ + +package io.xeres.app.net.protocol; + +import io.xeres.app.net.protocol.PeerAddress.Type; +import org.junit.jupiter.api.Test; + +import java.util.Optional; + +import static io.xeres.app.net.protocol.PeerAddress.Type.*; +import static org.junit.jupiter.api.Assertions.*; + +class PeerAddressTest +{ + /** + * Builds a PeerAddress from a string like "85.123.33.21:21232" + */ + @Test + void PeerAddress_FromIpAndPort_OK() + { + String IP_AND_PORT = "85.123.33.21:21232"; + PeerAddress peerAddress = PeerAddress.fromIpAndPort(IP_AND_PORT); + + assertEquals(Optional.of(IP_AND_PORT), peerAddress.getAddress()); + } + + @Test + void PeerAddress_FromIpAndPort_IllegalIpOctetsOverflow_Fail() + { + PeerAddress peerAddress = PeerAddress.fromIpAndPort("500.500.500.500:21232"); + + assertFalse(peerAddress.isValid()); + } + + @Test + void PeerAddress_FromIpAndPort_IllegalIpOctetMissing_Fail() + { + PeerAddress peerAddress = PeerAddress.fromIpAndPort("85.123.33:21232"); + + assertFalse(peerAddress.isValid()); + } + + @Test + void PeerAddress_FromIpAndPort_IllegalIpZeroPrefix_Fail() + { + PeerAddress peerAddress = PeerAddress.fromIpAndPort("85.123.33.01:21232"); + + assertFalse(peerAddress.isValid()); + } + + @Test + void PeerAddress_FromIpAndPort_IllegalIpNotANumber_Fail() + { + PeerAddress peerAddress = PeerAddress.fromIpAndPort("85.123.33.a:21232"); + + assertFalse(peerAddress.isValid()); + } + + @Test + void PeerAddress_FromIpAndPort_IllegalPortNotANumber_Fail() + { + PeerAddress peerAddress = PeerAddress.fromIpAndPort("85.123.33.1:a"); + + assertFalse(peerAddress.isValid()); + } + + @Test + void PeerAddress_FromIpAndPort_SeparatorButMissingPort_Fail() + { + PeerAddress peerAddress = PeerAddress.fromIpAndPort("85.123.33.1:"); + + assertFalse(peerAddress.isValid()); + } + + @Test + void PeerAddress_FromIpAndPort_SeparatorButMissingIp_Fail() + { + PeerAddress peerAddress = PeerAddress.fromIpAndPort(":2323"); + + assertFalse(peerAddress.isValid()); + } + + /** + * This kind of IP is legal (ie. "ping 127.1" will work) but we don't want it as it's confusing. + */ + @Test + void PeerAddress_FromIpAndPort_LegalIpButNotWanted_Fail() + { + PeerAddress peerAddress = PeerAddress.fromIpAndPort("85.1:21232"); + + assertFalse(peerAddress.isValid()); + } + + /** + * That one too. Even more messed up. + */ + @Test + void PeerAddress_FromIpAndPort_LegalIpButNotWanted2_Fail() + { + PeerAddress peerAddress = PeerAddress.fromIpAndPort("85.65530:21232"); + + assertFalse(peerAddress.isValid()); + } + + @Test + void PeerAddress_FromIpAndPort_LowPort_Fail() + { + PeerAddress peerAddress = PeerAddress.fromIpAndPort("85.123.33.21:0"); + + assertFalse(peerAddress.isValid()); + } + + @Test + void PeerAddress_FromIpAndPort_IllegalPort_Fail() + { + PeerAddress peerAddress = PeerAddress.fromIpAndPort("85.123.33.21:65537"); + + assertFalse(peerAddress.isValid()); + } + + @Test + void PeerAddress_FromIpAndPort_Bullshit_Fail() + { + PeerAddress peerAddress = PeerAddress.fromIpAndPort("2384902378237892"); + + assertFalse(peerAddress.isValid()); + } + + @Test + void PeerAddress_FromUrl_OK() + { + PeerAddress peerAddress = PeerAddress.fromUrl("ipv4://194.28.22.1:2233"); + + assertTrue(peerAddress.isValid()); + } + + @Test + void PeerAddress_FromUrl_MissingPort_Fail() + { + PeerAddress peerAddress = PeerAddress.fromUrl("ipv4://194.28.22.1"); + + assertFalse(peerAddress.isValid()); + } + + @Test + void PeerAddress_FromUrl_Invalid_Fail() + { + PeerAddress peerAddress = PeerAddress.fromUrl("ipv666://23sd.2343.2487.asdk"); + + assertFalse(peerAddress.isValid()); + } + + @Test + void PeerAddress_FromAddress_OK() + { + PeerAddress peerAddress = PeerAddress.fromAddress("194.28.22.1:1026"); + + assertTrue(peerAddress.isValid()); + } + + @Test + void PeerAddress_FromAddress2_OK() + { + PeerAddress peerAddress = PeerAddress.fromAddress("1.0.0.1:1026"); + + assertTrue(peerAddress.isValid()); + } + + @Test + void PeerAddress_FromAddress_MissingPort_Fail() + { + PeerAddress peerAddress = PeerAddress.fromAddress("194.28.22.1"); + + assertFalse(peerAddress.isValid()); + } + + @Test + void PeerAddress_FromIpAndPort_NonRoutableButLocalhost_Fail() + { + PeerAddress peerAddress = PeerAddress.fromIpAndPort("127.0.0.1:21232"); + + assertFalse(peerAddress.isValid()); + } + + @Test + void PeerAddress_FromIpAndPort_NotPublicButPrivateLan_OK() + { + PeerAddress peerAddress = PeerAddress.fromIpAndPort("192.168.1.5:21232"); + + assertTrue(peerAddress.isValid()); + } + + @Test + void PeerAddress_FromIpAndPort_NonRoutableButNetwork_Fail() + { + PeerAddress peerAddress = PeerAddress.fromIpAndPort("0.0.0.0:21232"); + + assertFalse(peerAddress.isValid()); + } + + @Test + void PeerAddress_FromIpAndPort_NonRoutable3_Fail() + { + PeerAddress peerAddress = PeerAddress.fromIpAndPort("255.255.255.255:21232"); + + assertFalse(peerAddress.isValid()); + } + + @Test + void PeerAddress_FromIpAndPort_BroadcastConventionButRoutable_OK() + { + PeerAddress peerAddress = PeerAddress.fromIpAndPort("1.1.1.255:21232"); + + assertTrue(peerAddress.isValid()); + } + + @Test + void PeerAddress_FromIpAndPort_NonRoutable5_Fail() + { + PeerAddress peerAddress = PeerAddress.fromIpAndPort("0.1.1.1:21232"); + + assertFalse(peerAddress.isValid()); + } + + @Test + void PeerAddress_FromIpAndPort_NetworkConventionButRoutable_OK() + { + PeerAddress peerAddress = PeerAddress.fromIpAndPort("1.1.1.0:21232"); + + assertTrue(peerAddress.isValid()); + } + + /** + * Tor v2 is not supported anymore + */ + @Test + void PeerAddress_FromTor_v2_Fail() + { + PeerAddress peerAddress = PeerAddress.fromOnion("expyuzz4wqqyqhjn.onion"); + + assertFalse(peerAddress.isValid()); + } + + @Test + void PeerAddress_FromTor_v3_OK() + { + PeerAddress peerAddress = PeerAddress.fromOnion("xpxduj55x2j27l2qytu2tcetykyfxbjbafin3x4i3ywddzphkbrd3jyd.onion"); + + assertTrue(peerAddress.isValid()); + assertEquals(Type.TOR, peerAddress.getType()); + } + + @Test + void PeerAddress_FromTor_WrongAddress_Fail() + { + PeerAddress peerAddress = PeerAddress.fromOnion("192.168.1.2:8080"); + + assertFalse(peerAddress.isValid()); + assertFalse(peerAddress.isHidden()); + } + + @Test + void PeerAddress_FromHidden_OK() + { + PeerAddress peerAddress = PeerAddress.fromHidden("xpxduj55x2j27l2qytu2tcetykyfxbjbafin3x4i3ywddzphkbrd3jyd.onion"); + + assertTrue(peerAddress.isValid()); + assertTrue(peerAddress.isHidden()); + } + + @Test + void PeerAddress_FromHidden_WrongAddress_Fail() + { + PeerAddress peerAddress = PeerAddress.fromHidden("192.168.1.2:8080"); + + assertFalse(peerAddress.isValid()); + assertFalse(peerAddress.isHidden()); + } + + @Test + void PeerAddress_Type_Enum_Order() + { + assertEquals(0, INVALID.ordinal()); + assertEquals(1, IPV4.ordinal()); + assertEquals(2, IPV6.ordinal()); + assertEquals(3, TOR.ordinal()); + assertEquals(4, HOSTNAME.ordinal()); + assertEquals(5, I2P.ordinal()); + } +} diff --git a/app/src/test/java/io/xeres/app/net/protocol/tor/OnionAddressTest.java b/app/src/test/java/io/xeres/app/net/protocol/tor/OnionAddressTest.java new file mode 100644 index 000000000..228b1483e --- /dev/null +++ b/app/src/test/java/io/xeres/app/net/protocol/tor/OnionAddressTest.java @@ -0,0 +1,48 @@ +/* + * Copyright (c) 2019-2021 by David Gerber - https://zapek.com + * + * This file is part of Xeres. + * + * Xeres is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Xeres is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Xeres. If not, see . + */ + +package io.xeres.app.net.protocol.tor; + +import io.xeres.testutils.TestUtils; +import org.junit.jupiter.api.Test; + +import static io.xeres.app.net.protocol.tor.OnionAddress.isValidAddress; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +class OnionAddressTest +{ + @Test + void OnionAddress_NoInstance_OK() throws NoSuchMethodException + { + TestUtils.assertUtilityClass(OnionAddress.class); + } + + @Test + void OnionAddress_IsValidAddress_OK() + { + assertTrue(isValidAddress("answerszuvs3gg2l64e6hmnryudl5zgrmwm3vh65hzszdghblddvfiqd.onion")); + } + + @Test + void OnionAddress_IsValidAddress_Fail() + { + assertFalse(isValidAddress("3g2upl4pq6kufc4m.onion")); + } +} diff --git a/app/src/test/java/io/xeres/app/net/upnp/ControlPointTest.java b/app/src/test/java/io/xeres/app/net/upnp/ControlPointTest.java new file mode 100644 index 000000000..7999a8d82 --- /dev/null +++ b/app/src/test/java/io/xeres/app/net/upnp/ControlPointTest.java @@ -0,0 +1,97 @@ +/* + * Copyright (c) 2019-2021 by David Gerber - https://zapek.com + * + * This file is part of Xeres. + * + * Xeres is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Xeres is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Xeres. If not, see . + */ + +package io.xeres.app.net.upnp; + +import io.xeres.testutils.FakeHTTPServer; +import io.xeres.testutils.TestUtils; +import org.junit.jupiter.api.Test; + +import java.io.IOException; +import java.net.URI; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; + +class ControlPointTest +{ + @Test + void ControlPoint_NoInstance_OK() throws NoSuchMethodException + { + TestUtils.assertUtilityClass(ControlPoint.class); + } + + @Test + void ControlPoint_AddPortMapping_OK() throws IOException + { + var fakeHTTPServer = new FakeHTTPServer("/control", 200, null); + + boolean added = ControlPoint.addPortMapping( + URI.create("http://localhost:" + FakeHTTPServer.LOCAL_PORT + "/control").toURL(), + "urn:schemas-upnp-org:service:WANIPConnection:1", + "192.168.1.78", + 2000, + 2000, + 3600, + Protocol.TCP + ); + assertTrue(added); + + fakeHTTPServer.shutdown(); + } + + @Test + void ControlPoint_RemovePortMapping_OK() throws IOException + { + var fakeHTTPServer = new FakeHTTPServer("/control", 200, null); + + boolean removed = ControlPoint.removePortMapping( + URI.create("http://localhost:" + FakeHTTPServer.LOCAL_PORT + "/control").toURL(), + "urn:schemas-upnp-org:service:WANIPConnection:1", + 2000, + Protocol.TCP + ); + assertTrue(removed); + + fakeHTTPServer.shutdown(); + } + + @Test + void ControlPoint_GetExternalIPAddress_OK() throws IOException + { + String responseBody = "" + + "" + + "" + + "1.1.1.1" + + "" + + "" + + ""; + + var fakeHTTPServer = new FakeHTTPServer("/control", 200, responseBody.getBytes()); + + String response = ControlPoint.getExternalIpAddress( + URI.create("http://localhost:" + FakeHTTPServer.LOCAL_PORT + "/control").toURL(), + "urn:schemas-upnp-org:service:WANIPConnection:1" + ); + + assertEquals("1.1.1.1", response); + + fakeHTTPServer.shutdown(); + } +} diff --git a/app/src/test/java/io/xeres/app/net/upnp/DeviceTest.java b/app/src/test/java/io/xeres/app/net/upnp/DeviceTest.java new file mode 100644 index 000000000..ef9c24414 --- /dev/null +++ b/app/src/test/java/io/xeres/app/net/upnp/DeviceTest.java @@ -0,0 +1,87 @@ +/* + * Copyright (c) 2019-2021 by David Gerber - https://zapek.com + * + * This file is part of Xeres. + * + * Xeres is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Xeres is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Xeres. If not, see . + */ + +package io.xeres.app.net.upnp; + +import io.xeres.testutils.FakeHTTPServer; +import org.junit.jupiter.api.Test; +import org.springframework.util.ResourceUtils; + +import java.io.IOException; +import java.net.InetSocketAddress; +import java.nio.ByteBuffer; +import java.nio.file.Files; + +import static org.junit.jupiter.api.Assertions.*; + +class DeviceTest +{ + @Test + void Device_From_OK() throws IOException + { + var inetSocketAddress = new InetSocketAddress(1068); + String httpuReply = "HTTP/1.1 200 OK\n" + + "CACHE-CONTROL: max-age=120\n" + + "ST: urn:schemas-upnp-org:device:InternetGatewayDevice:1\n" + + "USN: uuid:3ddcd1d3-2380-45f5-b069-88d7f644f8d8::urn:schemas-upnp-org:device:InternetGatewayDevice:1\n" + + "EXT:\n" + + "SERVER: AsusWRT/384.13 UPnP/1.1 MiniUPnPd/2.1\n" + + "LOCATION: http://localhost:" + FakeHTTPServer.LOCAL_PORT + "/rootDesc.xml\n" + + "OPT: \"http://schemas.upnp.org/upnp/1/0/\"; ns=01\n" + + "01-NLS: 1594920600\n" + + "BOOTID.UPNP.ORG: 1594920600\n" + + "CONFIGID.UPNP.ORG: 1337\n" + + "\n"; + + byte[] routerReply = Files.readAllBytes(ResourceUtils.getFile(ResourceUtils.CLASSPATH_URL_PREFIX + "upnp/routers/RT-AC87U.xml").toPath()); + var fakeHTTPServer = new FakeHTTPServer("/rootDesc.xml", 200, routerReply); + + Device device = Device.from( + inetSocketAddress, + ByteBuffer.wrap(httpuReply.getBytes()) + ); + assertTrue(device.isValid()); + assertFalse(device.isInvalid()); + assertEquals(inetSocketAddress, device.getInetSocketAddress()); + assertTrue(device.hasLocation()); + assertEquals("http://localhost:" + FakeHTTPServer.LOCAL_PORT + "/rootDesc.xml", device.getLocationUrl().toString()); + assertTrue(device.hasServer()); + assertEquals("AsusWRT/384.13 UPnP/1.1 MiniUPnPd/2.1", device.getServer()); + assertTrue(device.hasUsn()); + assertEquals("uuid:3ddcd1d3-2380-45f5-b069-88d7f644f8d8::urn:schemas-upnp-org:device:InternetGatewayDevice:1", device.getUsn()); + + device.addControlPoint(); + assertTrue(device.hasControlPoint()); + + assertTrue(device.hasControlUrl()); + assertEquals("http://localhost:" + FakeHTTPServer.LOCAL_PORT + "/ctl/IPConn", device.getControlUrl().toString()); + assertTrue(device.hasManufacturer()); + assertEquals("ASUSTek", device.getManufacturer()); + assertEquals("http://www.asus.com/", device.getManufacturerUrl().toString()); + assertTrue(device.hasModelName()); + assertEquals("RT-AC87U", device.getModelName()); + assertTrue(device.hasPresentationUrl()); + assertEquals("http://192.168.1.1:80/", device.getPresentationUrl().toString()); + assertTrue(device.hasSerialNumber()); + assertEquals("88:d7:f6:44:f8:d8", device.getSerialNumber()); + assertEquals("urn:schemas-upnp-org:service:WANIPConnection:1", device.getServiceType()); + + fakeHTTPServer.shutdown(); + } +} diff --git a/app/src/test/java/io/xeres/app/net/upnp/PortMappingTest.java b/app/src/test/java/io/xeres/app/net/upnp/PortMappingTest.java new file mode 100644 index 000000000..318d93494 --- /dev/null +++ b/app/src/test/java/io/xeres/app/net/upnp/PortMappingTest.java @@ -0,0 +1,57 @@ +/* + * Copyright (c) 2019-2021 by David Gerber - https://zapek.com + * + * This file is part of Xeres. + * + * Xeres is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Xeres is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Xeres. If not, see . + */ + +package io.xeres.app.net.upnp; + +import org.junit.jupiter.api.Test; + +import static io.xeres.app.net.upnp.Protocol.TCP; +import static io.xeres.app.net.upnp.Protocol.UDP; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotEquals; + +class PortMappingTest +{ + @Test + void PortMapping_Compare_OK() + { + var mapping1 = new PortMapping(1025, TCP); + var mapping2 = new PortMapping(1025, TCP); + + assertEquals(mapping1, mapping2); + } + + @Test + void PortMapping_Compare_InequalPort_Fail() + { + var mapping1 = new PortMapping(1025, TCP); + var mapping2 = new PortMapping(1026, TCP); + + assertNotEquals(mapping1, mapping2); + } + + @Test + void PortMapping_Compare_InequalProtocols_Fail() + { + var mapping1 = new PortMapping(1025, TCP); + var mapping2 = new PortMapping(1025, UDP); + + assertNotEquals(mapping1, mapping2); + } +} diff --git a/app/src/test/java/io/xeres/app/net/upnp/SoapTest.java b/app/src/test/java/io/xeres/app/net/upnp/SoapTest.java new file mode 100644 index 000000000..f31a767ab --- /dev/null +++ b/app/src/test/java/io/xeres/app/net/upnp/SoapTest.java @@ -0,0 +1,117 @@ +/* + * Copyright (c) 2019-2021 by David Gerber - https://zapek.com + * + * This file is part of Xeres. + * + * Xeres is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Xeres is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Xeres. If not, see . + */ + +package io.xeres.app.net.upnp; + +import io.xeres.testutils.FakeHTTPServer; +import io.xeres.testutils.TestUtils; +import org.junit.jupiter.api.Test; +import org.springframework.http.ResponseEntity; +import org.w3c.dom.Document; +import org.w3c.dom.NodeList; +import org.xml.sax.SAXException; + +import javax.xml.namespace.NamespaceContext; +import javax.xml.parsers.DocumentBuilderFactory; +import javax.xml.parsers.ParserConfigurationException; +import javax.xml.xpath.XPath; +import javax.xml.xpath.XPathException; +import javax.xml.xpath.XPathFactory; +import javax.xml.xpath.XPathNodes; +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.net.HttpURLConnection; +import java.net.URI; +import java.util.Iterator; +import java.util.LinkedHashMap; +import java.util.Map; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +class SoapTest +{ + private static final String SERVICE_TYPE = "urn:schemas-upnp-org:service:WANIPConnection:1"; + private static final String ACTION = "AddPortMapping"; + + @Test + void Soap_NoInstance_OK() throws NoSuchMethodException + { + TestUtils.assertUtilityClass(Soap.class); + } + + @Test + void Soap_SendRequest_OK() throws IOException, ParserConfigurationException, SAXException, XPathException + { + String KEY1 = "NewExternalPort", KEY2 = "NewProtocol"; + String VALUE1 = "1234", VALUE2 = "TCP"; + var fakeHTTPServer = new FakeHTTPServer("/soaptest.xml", HttpURLConnection.HTTP_OK, "OK".getBytes()); + + LinkedHashMap args = new LinkedHashMap<>(2); + args.put(KEY1, VALUE1); + args.put(KEY2, VALUE2); + + ResponseEntity responseEntity = Soap.sendRequest(URI.create("http://localhost:" + FakeHTTPServer.LOCAL_PORT + "/soaptest.xml").toURL(), SERVICE_TYPE, ACTION, args); + assertEquals("OK", responseEntity.getBody()); + + DocumentBuilderFactory documentBuilderFactory = DocumentBuilderFactory.newInstance(); + documentBuilderFactory.setNamespaceAware(true); + Document document = documentBuilderFactory.newDocumentBuilder().parse(new ByteArrayInputStream(fakeHTTPServer.getRequestBody())); + assertEquals("1.0", document.getXmlVersion()); + + XPath xPath = XPathFactory.newInstance().newXPath(); + xPath.setNamespaceContext(createNameSpaceContext(Map.of( + "s", "http://schemas.xmlsoap.org/soap/envelope/", + "u", SERVICE_TYPE))); + XPathNodes nodes = xPath.evaluateExpression("//s:Envelope//s:Body//u:" + ACTION, document, XPathNodes.class); + assertEquals(1, nodes.size()); + + assertEquals("u:" + ACTION, nodes.get(0).getNodeName()); + NodeList childNodes = nodes.get(0).getChildNodes(); + assertEquals(KEY1, childNodes.item(0).getNodeName()); + assertEquals(VALUE1, childNodes.item(0).getTextContent()); + assertEquals(KEY2, childNodes.item(1).getNodeName()); + assertEquals(VALUE2, childNodes.item(1).getTextContent()); + + fakeHTTPServer.shutdown(); + } + + private NamespaceContext createNameSpaceContext(Map uris) + { + return new NamespaceContext() + { + @Override + public String getNamespaceURI(String prefix) + { + return uris.get(prefix); + } + + @Override + public String getPrefix(String namespaceURI) + { + return null; + } + + @Override + public Iterator getPrefixes(String namespaceURI) + { + return null; + } + }; + } +} diff --git a/app/src/test/java/io/xeres/app/net/upnp/UPNPServiceTest.java b/app/src/test/java/io/xeres/app/net/upnp/UPNPServiceTest.java new file mode 100644 index 000000000..57dc7f53c --- /dev/null +++ b/app/src/test/java/io/xeres/app/net/upnp/UPNPServiceTest.java @@ -0,0 +1,50 @@ +/* + * Copyright (c) 2019-2021 by David Gerber - https://zapek.com + * + * This file is part of Xeres. + * + * Xeres is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Xeres is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Xeres. If not, see . + */ + +package io.xeres.app.net.upnp; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.springframework.test.context.junit.jupiter.SpringExtension; + +import java.util.concurrent.TimeUnit; + +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +@ExtendWith(SpringExtension.class) +class UPNPServiceTest +{ + @InjectMocks + private UPNPService upnpService; + + @Test + void UPNPService_StartStop_OK() throws InterruptedException + { + upnpService.start("127.0.0.1", 1901); // nothing should reply in there + + TimeUnit.SECONDS.sleep(2); + assertTrue(upnpService.isRunning()); + + upnpService.stop(); + upnpService.waitForTermination(); + assertFalse(upnpService.isRunning()); + } +} diff --git a/app/src/test/java/io/xeres/app/service/IdentityServiceTest.java b/app/src/test/java/io/xeres/app/service/IdentityServiceTest.java new file mode 100644 index 000000000..bbbe592bd --- /dev/null +++ b/app/src/test/java/io/xeres/app/service/IdentityServiceTest.java @@ -0,0 +1,153 @@ +/* + * Copyright (c) 2019-2021 by David Gerber - https://zapek.com + * + * This file is part of Xeres. + * + * Xeres is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Xeres is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Xeres. If not, see . + */ + +package io.xeres.app.service; + +import io.xeres.app.crypto.pgp.PGP; +import io.xeres.app.database.model.identity.Identity; +import io.xeres.app.database.model.identity.IdentityFakes; +import io.xeres.app.database.model.profile.ProfileFakes; +import io.xeres.app.database.repository.GxsIdRepository; +import io.xeres.app.database.repository.IdentityRepository; +import io.xeres.app.xrs.service.gxsid.item.GxsIdGroupItem; +import io.xeres.common.id.ProfileFingerprint; +import io.xeres.common.identity.Type; +import org.bouncycastle.jce.provider.BouncyCastleProvider; +import org.bouncycastle.openpgp.PGPException; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.ArgumentCaptor; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.springframework.test.context.junit.jupiter.SpringExtension; + +import java.io.IOException; +import java.security.Security; +import java.security.cert.CertificateException; +import java.time.Instant; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +@ExtendWith(SpringExtension.class) +class IdentityServiceTest +{ + @Mock + private PrefsService prefsService; + + @Mock + private ProfileService profileService; + + @Mock + private GxsIdRepository gxsIdRepository; + + @Mock + private IdentityRepository identityRepository; + + @Mock + private GxsExchangeService gxsExchangeService; + + @InjectMocks + private IdentityService identityService; + + @BeforeAll + static void setup() + { + Security.addProvider(new BouncyCastleProvider()); + } + + @Test + void IdentityService_CreateOwnIdentity_Anonymous_OK() throws PGPException, CertificateException, IOException + { + var NAME = "test"; + var identity = IdentityFakes.createOwnIdentity(NAME, Type.ANONYMOUS); + + when(prefsService.isOwnProfilePresent()).thenReturn(true); + when(prefsService.hasOwnLocation()).thenReturn(true); + when(identityRepository.save(any(Identity.class))).thenReturn(identity); + when(gxsIdRepository.save(any(GxsIdGroupItem.class))).thenAnswer(invocation -> { + var gxsIdGroupItem = (GxsIdGroupItem) invocation.getArguments()[0]; + gxsIdGroupItem.setPublished(Instant.now()); + return gxsIdGroupItem; + }); + + var id = identityService.createOwnIdentity(NAME, Type.ANONYMOUS); + + assertEquals(identity.getId(), id); + + ArgumentCaptor gxsIdGroupItem = ArgumentCaptor.forClass(GxsIdGroupItem.class); + verify(gxsIdRepository).save(gxsIdGroupItem.capture()); + assertEquals(NAME, gxsIdGroupItem.getValue().getName()); + verify(identityRepository).save(any(Identity.class)); + } + + @Test + void IdentityService_CreateOwnIdentity_Signed_OK() throws PGPException, CertificateException, IOException + { + var NAME = "test"; + var identity = IdentityFakes.createOwnIdentity(NAME, Type.SIGNED); + + var encodedKey = new byte[]{-107, 1, 30, 4, 96, -83, 89, -119, 1, 2, 0, -124, 36, -16, 89, 77, 70, 111, 82, 42, 104, 115, 27, 52, -67, 56, -116, 80, 71, 109, -9, + 78, -113, 115, -22, -35, 97, 121, 34, -118, 90, -6, -68, 113, 78, -58, -120, -4, -123, -1, 46, 10, -19, 122, -84, 21, -24, 118, 82, 12, -1, 45, -56, -94, -21, -25, -3, -68, 17, 45, + 9, -26, -33, 86, -53, 0, 17, 1, 0, 1, -2, 3, 3, 2, 120, 82, -62, 47, -20, 15, -47, -114, 96, -60, -67, 67, 56, -82, 79, -17, 82, -40, 17, 72, 39, -53, -72, 25, 52, -94, 103, -31, + 92, -51, 53, -29, 119, -26, 20, 81, 94, -29, -20, 104, 103, 56, -53, -53, 28, 6, -82, -33, 92, -31, -18, -4, 73, 55, 97, -89, 38, -21, 123, 30, -28, 76, -122, 20, 89, -28, -112, + -29, 32, -116, -75, -19, -113, 123, -23, -42, 122, 13, 1, -46, -70, -69, 87, -41, -104, -49, 101, 22, 79, -63, -112, -120, 79, 25, 16, -2, -77, 118, 110, -109, -33, -100, -11, + -126, -73, -64, 125, 56, 101, 49, -89, 19, -61, 125, 103, 121, 82, -15, 109, 2, 105, -103, -11, 31, -68, -117, -81, -14, 7, -9, 98, 18, 96, -26, 70, 66, -64, 108, -2, -6, 114, -13, + 44, -103, 81, -28, 80, 115, 124, 74, -28, -53, 53, -44, -118, 20, -94, -113, -43, 109, 111, 82, -21, 34, 80, -50, 62, 127, -38, -10, 108, -49, -123, 44, -39, 116, -90, 61, 41, -40, + -127, -84, 111, -127, -68, -75, 106, -9, -81, 37, -40, -120, 36, 62, 12, 45, 15, -88, 9, -51, -24, -96, 68, -38, 125, -76, 4, 116, 101, 115, 116, -120, 92, 4, 16, 1, 2, 0, 6, 5, 2, + 96, -83, 89, -119, 0, 10, 9, 16, -119, -55, 33, -4, 60, -108, 116, -23, -92, -19, 1, -4, 10, -89, 1, 44, 82, -29, 24, 104, -128, -73, -96, 122, -38, 67, -120, 18, 62, 10, 3, 95, 27, + -51, -45, -114, -113, -93, 118, 13, -20, 3, -35, 8, 15, 97, 27, 76, 20, 9, 78, 74, -24, 27, -99, -58, -125, -69, -103, -13, 50, -83, -117, -115, -123, 25, 52, 39, -122, -22, 81, 46, + 84, 22, -52, 17}; + + var secretKey = PGP.getPGPSecretKey(encodedKey); + var publicKey = secretKey.getPublicKey(); + var fingerprint = publicKey.getFingerprint(); + + var ownProfile = ProfileFakes.createProfile(NAME, PGP.getPGPIdentifierFromFingerprint(fingerprint), fingerprint, publicKey.getEncoded()); + + ownProfile.setProfileFingerprint(new ProfileFingerprint(secretKey.getPublicKey().getFingerprint())); + ownProfile.setPgpPublicKeyData(secretKey.getPublicKey().getEncoded()); + + when(prefsService.isOwnProfilePresent()).thenReturn(true); + when(prefsService.hasOwnLocation()).thenReturn(true); + when(profileService.getOwnProfile()).thenReturn(ownProfile); + when(prefsService.getSecretProfileKey()).thenReturn(encodedKey); + when(identityRepository.save(any(Identity.class))).thenReturn(identity); + when(gxsIdRepository.save(any(GxsIdGroupItem.class))).thenAnswer(invocation -> { + var gxsIdGroupItem = (GxsIdGroupItem) invocation.getArguments()[0]; + gxsIdGroupItem.setPublished(Instant.now()); + return gxsIdGroupItem; + }); + + var id = identityService.createOwnIdentity(NAME, Type.SIGNED); + + assertEquals(identity.getId(), id); + + ArgumentCaptor gxsIdGroupItem = ArgumentCaptor.forClass(GxsIdGroupItem.class); + verify(gxsIdRepository).save(gxsIdGroupItem.capture()); + assertEquals(NAME, gxsIdGroupItem.getValue().getName()); + assertNotNull(gxsIdGroupItem.getValue().getProfileHash()); + assertNotNull(gxsIdGroupItem.getValue().getProfileSignature()); + verify(identityRepository).save(any(Identity.class)); + } +} diff --git a/app/src/test/java/io/xeres/app/service/LocationServiceTest.java b/app/src/test/java/io/xeres/app/service/LocationServiceTest.java new file mode 100644 index 000000000..188d193b9 --- /dev/null +++ b/app/src/test/java/io/xeres/app/service/LocationServiceTest.java @@ -0,0 +1,209 @@ +/* + * Copyright (c) 2019-2021 by David Gerber - https://zapek.com + * + * This file is part of Xeres. + * + * Xeres is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Xeres is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Xeres. If not, see . + */ + +package io.xeres.app.service; + +import io.xeres.app.crypto.pgp.PGP; +import io.xeres.app.crypto.rsa.RSA; +import io.xeres.app.database.model.connection.Connection; +import io.xeres.app.database.model.connection.ConnectionFakes; +import io.xeres.app.database.model.location.Location; +import io.xeres.app.database.model.location.LocationFakes; +import io.xeres.app.database.model.profile.Profile; +import io.xeres.app.database.model.profile.ProfileFakes; +import io.xeres.app.database.repository.LocationRepository; +import io.xeres.app.net.protocol.PeerAddress; +import io.xeres.common.id.LocationId; +import org.bouncycastle.jce.provider.BouncyCastleProvider; +import org.bouncycastle.openpgp.PGPException; +import org.bouncycastle.openpgp.PGPSecretKey; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.springframework.data.domain.Pageable; +import org.springframework.data.domain.Slice; +import org.springframework.data.domain.SliceImpl; +import org.springframework.test.context.junit.jupiter.SpringExtension; + +import java.io.IOException; +import java.net.InetSocketAddress; +import java.security.KeyPair; +import java.security.NoSuchAlgorithmException; +import java.security.Security; +import java.security.cert.CertificateException; +import java.security.spec.InvalidKeySpecException; +import java.util.List; +import java.util.Optional; + +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.Mockito.*; + +@ExtendWith(SpringExtension.class) +class LocationServiceTest +{ + @Mock + private PrefsService prefsService; + + @Mock + private ProfileService profileService; + + @Mock + private LocationRepository locationRepository; + + @InjectMocks + private LocationService locationService; + + private static PGPSecretKey pgpSecretKey; + private static KeyPair keyPair; + private static Profile ownProfile; + + @BeforeAll + static void setup() throws PGPException, IOException + { + Security.addProvider(new BouncyCastleProvider()); + + pgpSecretKey = PGP.generateSecretKey("test", "", 512); + keyPair = RSA.generateKeys(512); + ownProfile = Profile.createProfile("test", pgpSecretKey.getKeyID(), pgpSecretKey.getPublicKey().getFingerprint(), pgpSecretKey.getPublicKey().getEncoded()); + } + + @Test + void LocationService_GenerateLocationKeys_OK() + { + when(prefsService.getLocationPrivateKeyData()).thenReturn(null); + + locationService.generateLocationKeys(); + + verify(prefsService).getLocationPrivateKeyData(); + verify(prefsService).saveLocationKeys(any(KeyPair.class)); + } + + @Test + void LocationService_GenerateLocationKeys_LocationAlreadyExists_OK() + { + when(prefsService.getLocationPrivateKeyData()).thenReturn(new byte[]{1}); + + verify(prefsService, times(0)).saveLocationKeys(any(KeyPair.class)); + } + + @Test + void LocationService_GenerateLocationCertificate_OK() throws NoSuchAlgorithmException, CertificateException, InvalidKeySpecException, IOException + { + when(prefsService.hasOwnLocation()).thenReturn(false); + when(prefsService.isOwnProfilePresent()).thenReturn(true); + when(prefsService.getSecretProfileKey()).thenReturn(pgpSecretKey.getEncoded()); + when(prefsService.getLocationPublicKeyData()).thenReturn(keyPair.getPublic().getEncoded()); + when(profileService.getOwnProfile()).thenReturn(ownProfile); + + locationService.generateLocationCertificate(); + + verify(prefsService).hasOwnLocation(); + verify(prefsService).isOwnProfilePresent(); + verify(prefsService).saveLocationCertificate(any(byte[].class)); + } + + @Test + void LocationService_GenerateLocationCertificate_LocationAlreadyExists_OK() throws CertificateException, InvalidKeySpecException, NoSuchAlgorithmException, IOException + { + when(prefsService.hasOwnLocation()).thenReturn(true); + + locationService.generateLocationCertificate(); + + verify(prefsService, times(0)).saveLocationCertificate(any(byte[].class)); + } + + @Test + void LocationService_GenerateLocationCertificate_MissingProfile_Fail() + { + when(prefsService.hasOwnLocation()).thenReturn(false); + when(prefsService.isOwnProfilePresent()).thenReturn(false); + + assertThatThrownBy(() -> locationService.generateLocationCertificate()) + .isInstanceOf(CertificateException.class) + .hasMessageContaining("without a profile"); + + verify(prefsService).hasOwnLocation(); + verify(prefsService).isOwnProfilePresent(); + } + + @Test + void LocationService_CreateLocation_OK() throws CertificateException, IOException + { + when(prefsService.isOwnProfilePresent()).thenReturn(true); + when(profileService.getOwnProfile()).thenReturn(ownProfile); + when(prefsService.getLocationId()).thenReturn(new LocationId()); + when(prefsService.getSecretProfileKey()).thenReturn(pgpSecretKey.getEncoded()); + when(prefsService.getLocationPublicKeyData()).thenReturn(keyPair.getPublic().getEncoded()); + when(profileService.getOwnProfile()).thenReturn(ownProfile); + + locationService.createOwnLocation("test"); + + verify(prefsService, times(2)).isOwnProfilePresent(); + verify(profileService, times(2)).getOwnProfile(); + verify(prefsService).getLocationId(); + verify(locationRepository).save(any(Location.class)); + // There's no way to reliably wait for the publisher's event since it's asynchronous + } + + @Test + void LocationService_GetConnectionsToConnectTo_OK() + { + Location location1 = LocationFakes.createLocation("test1", ownProfile); + location1.addConnection(ConnectionFakes.createConnection()); + Location location2 = LocationFakes.createLocation("test2", ownProfile); + location2.addConnection(ConnectionFakes.createConnection()); + location2.addConnection(ConnectionFakes.createConnection(PeerAddress.Type.IPV4, "1.2.3.4:1234")); + + List locations = List.of(location1, location2); + Slice slice = new SliceImpl<>(locations); + when(locationRepository.findAllByConnectedFalse(any(Pageable.class))).thenReturn(slice); + + List connections = locationService.getConnectionsToConnectTo(); + + assertEquals(2, connections.size()); + } + + @Test + void LocationService_SetConnected_OK() + { + Location location = LocationFakes.createLocation("foo", ProfileFakes.createProfile("foo", 1)); + + locationService.setConnected(location, new InetSocketAddress("127.0.0.1", 666)); + + assertTrue(location.isConnected()); + verify(locationRepository).save(any(Location.class)); + } + + @Test + void LocationService_SetDisconnected_OK() + { + long LOCATION_ID = 1; + Location location = LocationFakes.createLocation("foo", ProfileFakes.createProfile("foo", 1)); + location.setConnected(true); + + when(locationRepository.findById(LOCATION_ID)).thenReturn(Optional.of(location)); + + locationService.setDisconnected(location); + + assertFalse(location.isConnected()); + } +} diff --git a/app/src/test/java/io/xeres/app/service/PrefsServiceTest.java b/app/src/test/java/io/xeres/app/service/PrefsServiceTest.java new file mode 100644 index 000000000..78e12e3bb --- /dev/null +++ b/app/src/test/java/io/xeres/app/service/PrefsServiceTest.java @@ -0,0 +1,59 @@ +/* + * Copyright (c) 2019-2021 by David Gerber - https://zapek.com + * + * This file is part of Xeres. + * + * Xeres is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Xeres is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Xeres. If not, see . + */ + +package io.xeres.app.service; + +import io.xeres.app.database.model.prefs.Prefs; +import io.xeres.app.database.repository.PrefsRepository; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.springframework.test.context.junit.jupiter.SpringExtension; + +import java.util.Optional; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +@ExtendWith(SpringExtension.class) +class PrefsServiceTest +{ + @Mock + private PrefsRepository prefsRepository; + + @Mock + private Prefs prefs; + + @InjectMocks + private PrefsService prefsService; + + @Test + void PrefsService_SaveSecretProfileKey_OK() + { + when(prefsRepository.findById((byte) 1)).thenReturn(Optional.of(prefs)); + prefsService.init(); + + prefsService.saveSecretProfileKey(new byte[]{1}); + + verify(prefs).setPgpPrivateKeyData(any(byte[].class)); + verify(prefsRepository).save(any(Prefs.class)); + } +} diff --git a/app/src/test/java/io/xeres/app/service/ProfileServiceTest.java b/app/src/test/java/io/xeres/app/service/ProfileServiceTest.java new file mode 100644 index 000000000..2dfabf3ed --- /dev/null +++ b/app/src/test/java/io/xeres/app/service/ProfileServiceTest.java @@ -0,0 +1,145 @@ +/* + * Copyright (c) 2019-2021 by David Gerber - https://zapek.com + * + * This file is part of Xeres. + * + * Xeres is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Xeres is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Xeres. If not, see . + */ + +package io.xeres.app.service; + +import io.xeres.app.database.model.location.LocationFakes; +import io.xeres.app.database.model.profile.Profile; +import io.xeres.app.database.model.profile.ProfileFakes; +import io.xeres.app.database.repository.ProfileRepository; +import io.xeres.common.id.ProfileFingerprint; +import org.bouncycastle.jce.provider.BouncyCastleProvider; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.ArgumentCaptor; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.springframework.test.context.junit.jupiter.SpringExtension; + +import java.security.Security; +import java.util.Optional; + +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.Mockito.*; + +@ExtendWith(SpringExtension.class) +class ProfileServiceTest +{ + @Mock + private PrefsService prefsService; + + @Mock + private ProfileRepository profileRepository; + + @InjectMocks + private ProfileService profileService; + + @BeforeAll + static void setup() + { + Security.addProvider(new BouncyCastleProvider()); + } + + @Test + void ProfileService_GenerateProfileKeys_OK() + { + String NAME = "test"; + + when(prefsService.getSecretProfileKey()).thenReturn(null); + + assertTrue(profileService.generateProfileKeys(NAME)); + + verify(prefsService).getSecretProfileKey(); + + ArgumentCaptor profile = ArgumentCaptor.forClass(Profile.class); + verify(profileRepository).save(profile.capture()); + assertTrue(profile.getValue().getName().startsWith(NAME)); + verify(prefsService).saveSecretProfileKey(any(byte[].class)); + } + + @Test + void ProfileService_GenerateProfileKeys_AlreadyExists_Fail() + { + String NAME = "test"; + + when(prefsService.getSecretProfileKey()).thenReturn(new byte[]{1}); + + assertThatThrownBy(() -> profileService.generateProfileKeys(NAME)) + .isInstanceOf(IllegalStateException.class) + .hasMessageContaining("already exists"); + + verify(prefsService).getSecretProfileKey(); + verify(profileRepository, times(0)).save(any(Profile.class)); + verify(prefsService, times(0)).saveSecretProfileKey(any(byte[].class)); + } + + @Test + void ProfileService_GenerateProfileKeys_KeyIdTooShort_Fail() + { + String NAME = ""; + + when(prefsService.getSecretProfileKey()).thenReturn(null); + + assertThatThrownBy(() -> profileService.generateProfileKeys(NAME)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessageContaining("too short"); + + verify(prefsService).getSecretProfileKey(); + verify(profileRepository, times(0)).save(any(Profile.class)); + verify(prefsService, times(0)).saveSecretProfileKey(any(byte[].class)); + } + + @Test + void ProfileService_GenerateProfileKeys_KeyIdTooLong_Fail() + { + String NAME = "12345678900987654321123456789098765432120987676543432123456798765"; + + when(prefsService.getSecretProfileKey()).thenReturn(null); + + assertThatThrownBy(() -> profileService.generateProfileKeys(NAME)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessageContaining("too long"); + + verify(prefsService).getSecretProfileKey(); + verify(profileRepository, times(0)).save(any(Profile.class)); + verify(prefsService, times(0)).saveSecretProfileKey(any(byte[].class)); + } + + @Test + void ProfileService_CreateOrUpdateProfile_Update_OK() + { + Profile first = ProfileFakes.createProfile("first", 1); + first.addLocation(LocationFakes.createLocation("first location", first)); + + Profile second = ProfileFakes.createProfile("first", 1); + second.addLocation(LocationFakes.createLocation("second location", second)); + + when(profileRepository.findByProfileFingerprint(any(ProfileFingerprint.class))).thenReturn(Optional.of(first)); + when(profileRepository.save(any(Profile.class))).thenAnswer(mock -> mock.getArguments()[0]); + + Profile updated = profileService.createOrUpdateProfile(second).orElseThrow(); + + assertEquals(2, updated.getLocations().size()); + + // XXX: add the case where we "update" an existing location, not just add + } +} diff --git a/app/src/test/java/io/xeres/app/web/api/controller/AbstractControllerTest.java b/app/src/test/java/io/xeres/app/web/api/controller/AbstractControllerTest.java new file mode 100644 index 000000000..a55c07323 --- /dev/null +++ b/app/src/test/java/io/xeres/app/web/api/controller/AbstractControllerTest.java @@ -0,0 +1,76 @@ +/* + * Copyright (c) 2019-2020 by David Gerber - https://zapek.com + * + * This file is part of Xeres. + * + * Xeres is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Xeres is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Xeres. If not, see . + */ + +package io.xeres.app.web.api.controller; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.test.web.servlet.MockMvc; +import org.springframework.test.web.servlet.request.MockHttpServletRequestBuilder; + +import static org.springframework.http.MediaType.APPLICATION_JSON; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*; + +public abstract class AbstractControllerTest +{ + @Autowired + protected ObjectMapper objectMapper; + + @Autowired + protected MockMvc mvc; + + protected MockHttpServletRequestBuilder getJson(String uri) + { + return get(uri) + .accept(APPLICATION_JSON); + } + + protected MockHttpServletRequestBuilder postJson(String uri, Object body) + { + try + { + String json = objectMapper.writeValueAsString(body); + return post(uri) + .contentType(APPLICATION_JSON) + .accept(APPLICATION_JSON) + .content(json); + } + catch (JsonProcessingException e) + { + throw new RuntimeException(e); + } + } + + protected MockHttpServletRequestBuilder putJson(String uri, Object body) + { + try + { + String json = objectMapper.writeValueAsString(body); + return put(uri) + .contentType(APPLICATION_JSON) + .accept(APPLICATION_JSON) + .content(json); + } + catch (JsonProcessingException e) + { + throw new RuntimeException(e); + } + } +} diff --git a/app/src/test/java/io/xeres/app/web/api/controller/PathConfigTest.java b/app/src/test/java/io/xeres/app/web/api/controller/PathConfigTest.java new file mode 100644 index 000000000..864c255db --- /dev/null +++ b/app/src/test/java/io/xeres/app/web/api/controller/PathConfigTest.java @@ -0,0 +1,33 @@ +/* + * Copyright (c) 2019-2021 by David Gerber - https://zapek.com + * + * This file is part of Xeres. + * + * Xeres is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Xeres is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Xeres. If not, see . + */ + +package io.xeres.app.web.api.controller; + +import io.xeres.common.rest.PathConfig; +import io.xeres.testutils.TestUtils; +import org.junit.jupiter.api.Test; + +class PathConfigTest +{ + @Test + void PathConfig_NoInstance_OK() throws NoSuchMethodException + { + TestUtils.assertUtilityClass(PathConfig.class); + } +} diff --git a/app/src/test/java/io/xeres/app/web/api/controller/config/ConfigControllerTest.java b/app/src/test/java/io/xeres/app/web/api/controller/config/ConfigControllerTest.java new file mode 100644 index 000000000..d98cb6ac3 --- /dev/null +++ b/app/src/test/java/io/xeres/app/web/api/controller/config/ConfigControllerTest.java @@ -0,0 +1,281 @@ +/* + * Copyright (c) 2019-2021 by David Gerber - https://zapek.com + * + * This file is part of Xeres. + * + * Xeres is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Xeres is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Xeres. If not, see . + */ + +package io.xeres.app.web.api.controller.config; + +import io.xeres.app.database.model.connection.Connection; +import io.xeres.app.database.model.identity.IdentityFakes; +import io.xeres.app.database.model.location.Location; +import io.xeres.app.net.protocol.PeerAddress; +import io.xeres.app.service.IdentityService; +import io.xeres.app.service.LocationService; +import io.xeres.app.service.ProfileService; +import io.xeres.app.web.api.controller.AbstractControllerTest; +import io.xeres.common.identity.Type; +import io.xeres.common.rest.config.IpAddressRequest; +import io.xeres.common.rest.config.OwnIdentityRequest; +import io.xeres.common.rest.config.OwnLocationRequest; +import io.xeres.common.rest.config.OwnProfileRequest; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; +import org.springframework.boot.test.mock.mockito.MockBean; +import org.springframework.test.web.servlet.MockMvc; + +import java.security.cert.CertificateException; +import java.util.NoSuchElementException; +import java.util.Optional; + +import static io.xeres.common.rest.PathConfig.*; +import static org.hamcrest.Matchers.is; +import static org.mockito.Mockito.*; +import static org.springframework.http.MediaType.APPLICATION_JSON; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*; + +@WebMvcTest(ConfigController.class) +class ConfigControllerTest extends AbstractControllerTest +{ + private static final String BASE_URL = CONFIG_PATH; + + @MockBean + private ProfileService profileService; + + @MockBean + private LocationService locationService; + + @MockBean + private IdentityService identityService; + + @Autowired + public MockMvc mvc; + + @Test + void ConfigController_CreateProfile_OK() throws Exception + { + var profileRequest = new OwnProfileRequest("test node"); + + when(profileService.generateProfileKeys(profileRequest.name())).thenReturn(true); + + mvc.perform(postJson(BASE_URL + "/profile", profileRequest)) + .andExpect(status().isCreated()) + .andExpect(header().string("Location", "http://localhost" + PROFILES_PATH + "/" + 1L)); + + verify(profileService).generateProfileKeys(profileRequest.name()); + } + + @Test + void ConfigController_CreateProfile_Fail() throws Exception + { + var ownProfileRequest = new OwnProfileRequest("test node"); + + when(profileService.generateProfileKeys(ownProfileRequest.name())).thenReturn(false); + + mvc.perform(postJson(BASE_URL + "/profile", ownProfileRequest)) + .andExpect(status().isInternalServerError()); + + verify(profileService).generateProfileKeys(ownProfileRequest.name()); + } + + @Test + void ConfigController_CreateProfile_NameTooLong() throws Exception + { + var ownProfileRequest = new OwnProfileRequest("This name is way too long and there's no chance it ever gets created as a node"); + + mvc.perform(postJson(BASE_URL + "/profile", ownProfileRequest)) + .andExpect(status().isBadRequest()); + + verifyNoInteractions(profileService); + } + + @Test + void ConfigController_CreateProfile_NameTooShort() throws Exception + { + var ownProfileRequest = new OwnProfileRequest(""); + + mvc.perform(postJson(BASE_URL + "/profile", ownProfileRequest)) + .andExpect(status().isBadRequest()); + + verifyNoInteractions(profileService); + } + + @Test + void ConfigController_CreateProfile_MissingName() throws Exception + { + var ownProfileRequest = new OwnProfileRequest(null); + + mvc.perform(postJson(BASE_URL + "/profile", ownProfileRequest)) + .andExpect(status().isBadRequest()); + + verifyNoInteractions(profileService); + } + + @Test + void ConfigController_CreateLocation_OK() throws Exception + { + var ownLocationRequest = new OwnLocationRequest("test location"); + + mvc.perform(postJson(BASE_URL + "/location", ownLocationRequest)) + .andExpect(status().isCreated()); + + verify(locationService).createOwnLocation(anyString()); + } + + @Test + void ConfigController_CreateLocation_Fail() throws Exception + { + doThrow(CertificateException.class).when(locationService).createOwnLocation(anyString()); + + mvc.perform(post(BASE_URL + "/location") + .accept(APPLICATION_JSON)) + .andExpect(status().isInternalServerError()); + } + + @Test + void ConfigController_UpdateExternalIpAddress_Create_OK() throws Exception + { + String IP = "1.1.1.1"; + int PORT = 6667; + + when(locationService.findOwnLocation()).thenReturn(Optional.of(Location.createLocation("foo"))); + when(locationService.updateConnection(any(Location.class), any(PeerAddress.class))).thenReturn(LocationService.UpdateConnectionStatus.ADDED); + + var request = new IpAddressRequest(IP, PORT); + + mvc.perform(putJson(BASE_URL + "/externalIp", request)) + .andExpect(status().isCreated()) + .andExpect(header().string("Location", "http://localhost" + CONFIG_PATH + "/externalIp")); + + verify(locationService).updateConnection(any(Location.class), any(PeerAddress.class)); + } + + @Test + void ConfigController_UpdateExternalIpAddress_Update_OK() throws Exception + { + String IP = "1.1.1.1"; + int PORT = 6667; + + when(locationService.findOwnLocation()).thenReturn(Optional.of(Location.createLocation("foo"))); + when(locationService.updateConnection(any(Location.class), any(PeerAddress.class))).thenReturn(LocationService.UpdateConnectionStatus.UPDATED); + + var request = new IpAddressRequest(IP, PORT); + + + mvc.perform(putJson(BASE_URL + "/externalIp", request)) + .andExpect(status().isNoContent()); + + verify(locationService).updateConnection(any(Location.class), any(PeerAddress.class)); + } + + @Test + void ConfigController_UpdateExternalIpAddress_Update_NoConnection_Fail() throws Exception + { + String IP = "1.1.1.1"; + int PORT = 6667; + + when(locationService.findOwnLocation()).thenReturn(Optional.of(Location.createLocation("foo"))); + when(locationService.updateConnection(any(Location.class), any(PeerAddress.class))).thenThrow(NoSuchElementException.class); + + var request = new IpAddressRequest(IP, PORT); + + mvc.perform(putJson(BASE_URL + "/externalIp", request)) + .andExpect(status().isNotFound()); + + verify(locationService).updateConnection(any(Location.class), any(PeerAddress.class)); + } + + @Test + void ConfigController_UpdateExternalIpAddress_Update_WrongIp_Fail() throws Exception + { + String IP = "1.1.1.1.1"; + int PORT = 6667; + + when(locationService.updateConnection(any(Location.class), any(PeerAddress.class))).thenThrow(NoSuchElementException.class); + + var request = new IpAddressRequest(IP, PORT); + + mvc.perform(putJson(BASE_URL + "/externalIp", request)) + .andExpect(status().isInternalServerError()); + } + + @Test + void ConfigController_GetExternalIpAddress_OK() throws Exception + { + String IP = "1.1.1.1"; + int PORT = 6667; + + Location location = Location.createLocation("test"); + Connection connection = Connection.from(PeerAddress.from(IP, PORT)); + location.addConnection(connection); + + when(locationService.findOwnLocation()).thenReturn(Optional.of(location)); + + mvc.perform(getJson(BASE_URL + "/externalIp")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.ip", is(IP))) + .andExpect(jsonPath("$.port", is(PORT))); + } + + @Test + void ConfigController_GetExternalIpAddress_NoLocationOrIpAddress_OK() throws Exception + { + when(locationService.findOwnLocation()).thenReturn(Optional.empty()); + + mvc.perform(getJson(BASE_URL + "/externalIp")) + .andExpect(status().isNotFound()); + } + + @Test + void ConfigController_GetHostname_OK() throws Exception + { + String HOSTNAME = "foo.bar.com"; + + when(locationService.getHostname()).thenReturn(HOSTNAME); + + mvc.perform(getJson(BASE_URL + "/hostname")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.hostname", is(HOSTNAME))); + } + + @Test + void ConfigController_GetUsername_OK() throws Exception + { + String USERNAME = "foobar"; + when(locationService.getUsername()).thenReturn(USERNAME); + + mvc.perform(getJson(BASE_URL + "/username")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.username", is(USERNAME))); + } + + @Test + void ConfigController_CreateIdentity_Anonymous_OK() throws Exception + { + var identity = IdentityFakes.createOwnIdentity("test", Type.ANONYMOUS); + var identityRequest = new OwnIdentityRequest(identity.getGxsIdGroupItem().getName(), true); + + when(identityService.createOwnIdentity(identityRequest.name(), Type.ANONYMOUS)).thenReturn(identity.getId()); + + mvc.perform(postJson(BASE_URL + "/identity", identityRequest)) + .andExpect(status().isCreated()) + .andExpect(header().string("Location", "http://localhost" + IDENTITY_PATH + "/" + identity.getId())); + + verify(identityService).createOwnIdentity(identityRequest.name(), Type.ANONYMOUS); + } +} diff --git a/app/src/test/java/io/xeres/app/web/api/controller/profile/ProfileControllerTest.java b/app/src/test/java/io/xeres/app/web/api/controller/profile/ProfileControllerTest.java new file mode 100644 index 000000000..674d005ee --- /dev/null +++ b/app/src/test/java/io/xeres/app/web/api/controller/profile/ProfileControllerTest.java @@ -0,0 +1,197 @@ +/* + * Copyright (c) 2019-2021 by David Gerber - https://zapek.com + * + * This file is part of Xeres. + * + * Xeres is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Xeres is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Xeres. If not, see . + */ + +package io.xeres.app.web.api.controller.profile; + +import io.xeres.app.crypto.rsid.RSId; +import io.xeres.app.crypto.rsid.RSIdArmor; +import io.xeres.app.crypto.rsid.certificate.RSCertificateFakes; +import io.xeres.app.database.model.profile.Profile; +import io.xeres.app.database.model.profile.ProfileFakes; +import io.xeres.app.service.ProfileService; +import io.xeres.app.web.api.controller.AbstractControllerTest; +import io.xeres.common.rest.profile.CertificateRequest; +import org.bouncycastle.util.encoders.Base64; +import org.junit.jupiter.api.Test; +import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; +import org.springframework.boot.test.mock.mockito.MockBean; + +import java.util.List; +import java.util.NoSuchElementException; +import java.util.Optional; + +import static io.xeres.common.rest.PathConfig.PROFILES_PATH; +import static org.hamcrest.Matchers.is; +import static org.mockito.Mockito.*; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.delete; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*; + +@WebMvcTest(ProfileController.class) +class ProfileControllerTest extends AbstractControllerTest +{ + private static final String BASE_URL = PROFILES_PATH; + + @MockBean + private ProfileService profileService; + + @Test + void ProfileController_FindProfileById_OK() throws Exception + { + var expected = ProfileFakes.createProfile("test", 1); + + when(profileService.findProfileById(expected.getId())).thenReturn(Optional.of(expected)); + + mvc.perform(getJson(BASE_URL + "/" + expected.getId())) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.id", is((int) expected.getId()))) + .andExpect(jsonPath("$.name", is(expected.getName()))) + .andExpect(jsonPath("$.pgpFingerprint", is(Base64.toBase64String(expected.getProfileFingerprint().getBytes())))) + .andExpect(jsonPath("$.pgpPublicKeyData", is(Base64.toBase64String(expected.getPgpPublicKeyData())))) + .andExpect(jsonPath("$.accepted", is(expected.isAccepted()))) + .andExpect(jsonPath("$.trust", is(expected.getTrust().name()))); + + verify(profileService).findProfileById(expected.getId()); + } + + @Test + void ProfileController_FindProfileById_NotFound() throws Exception + { + long ID = 2L; + + when(profileService.findProfileById(ID)).thenReturn(Optional.empty()); + + mvc.perform(getJson(BASE_URL + "/" + ID)) + .andExpect(status().isNotFound()); + + verify(profileService).findProfileById(ID); + } + + @Test + void ProfileController_FindProfileByName_OK() throws Exception + { + var expected = ProfileFakes.createProfile("test", 1); + + when(profileService.findProfileByName(expected.getName())).thenReturn(Optional.of(expected)); + + mvc.perform(getJson(BASE_URL + "?name=" + expected.getName())) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.[0].id", is((int) expected.getId()))) + .andExpect(jsonPath("$.[0].name", is(expected.getName()))); + + verify(profileService).findProfileByName(expected.getName()); + } + + @Test + void ProfileController_FindProfileByName_NotFound() throws Exception + { + String NAME = "inexistant"; + + when(profileService.findProfileByName(NAME)).thenReturn(Optional.empty()); + + mvc.perform(getJson(BASE_URL + "?name=" + NAME)) + .andExpect(status().isOk()) + .andExpect(content().string("[]")); + + verify(profileService).findProfileByName(NAME); + } + + @Test + void ProfileController_FindProfiles_OK() throws Exception + { + var profile1 = ProfileFakes.createProfile("test1", 1); + var profile2 = ProfileFakes.createProfile("test2", 2); + var profiles = List.of(profile1, profile2); + + when(profileService.getAllProfiles()).thenReturn(profiles); + + mvc.perform(getJson(BASE_URL)) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.[0].id", is((int) profiles.get(0).getId()))); + + verify(profileService).getAllProfiles(); + } + + @Test + void ProfileController_CreateProfile_OK() throws Exception + { + var expected = ProfileFakes.createProfile("test", 1); + var profileRequest = new CertificateRequest(RSIdArmor.getArmored(RSCertificateFakes.createRSCertificate())); + + when(profileService.getProfileFromRSId(any(RSId.class), any(RSId.Type.class))).thenReturn(expected); + when(profileService.createOrUpdateProfile(any(Profile.class))).thenReturn(Optional.of(expected)); + + mvc.perform(postJson(BASE_URL, profileRequest)) + .andExpect(status().isCreated()) + .andExpect(header().string("Location", "http://localhost" + PROFILES_PATH + "/" + expected.getId())); + + verify(profileService).createOrUpdateProfile(any(Profile.class)); + } + + @Test + void ProfileController_CreateProfile_MissingCertificate() throws Exception + { + var profileRequest = new CertificateRequest(null); + + mvc.perform(postJson(BASE_URL, profileRequest)) + .andExpect(status().isBadRequest()); + } + + @Test + void ProfileController_CreateProfile_BrokenCertificate() throws Exception + { + var profileRequest = new CertificateRequest("foo"); + + mvc.perform(postJson(BASE_URL, profileRequest)) + .andExpect(status().isBadRequest()); + } + + @Test + void ProfileController_DeleteProfile_OK() throws Exception + { + long ID = 2; + + mvc.perform(delete(BASE_URL + "/" + ID)) + .andExpect(status().isNoContent()); + + verify(profileService).deleteProfile(ID); + } + + @Test + void ProfileController_DeleteProfile_NotFound() throws Exception + { + long ID = 2; + var profile = ProfileFakes.createProfile("test", ID); + + doThrow(NoSuchElementException.class).when(profileService).deleteProfile(ID); + + mvc.perform(delete(BASE_URL + "/" + ID)) + .andExpect(status().isNotFound()); + + verify(profileService).deleteProfile(ID); + } + + @Test + void ProfileController_DeleteProfile_Own() throws Exception + { + long ID = 1; + + mvc.perform(delete(BASE_URL + "/" + ID)) + .andExpect(status().isUnprocessableEntity()); + } +} diff --git a/app/src/test/java/io/xeres/app/xrs/item/ItemFactoryTest.java b/app/src/test/java/io/xeres/app/xrs/item/ItemFactoryTest.java new file mode 100644 index 000000000..2b2cb5ddb --- /dev/null +++ b/app/src/test/java/io/xeres/app/xrs/item/ItemFactoryTest.java @@ -0,0 +1,32 @@ +/* + * Copyright (c) 2019-2021 by David Gerber - https://zapek.com + * + * This file is part of Xeres. + * + * Xeres is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Xeres is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Xeres. If not, see . + */ + +package io.xeres.app.xrs.item; + +import io.xeres.testutils.TestUtils; +import org.junit.jupiter.api.Test; + +class ItemFactoryTest +{ + @Test + void ItemFactory_NoInstance_OK() throws NoSuchMethodException + { + TestUtils.assertUtilityClass(ItemFactory.class); + } +} diff --git a/app/src/test/java/io/xeres/app/xrs/item/ItemPriorityTest.java b/app/src/test/java/io/xeres/app/xrs/item/ItemPriorityTest.java new file mode 100644 index 000000000..2fdc99a65 --- /dev/null +++ b/app/src/test/java/io/xeres/app/xrs/item/ItemPriorityTest.java @@ -0,0 +1,36 @@ +/* + * Copyright (c) 2019-2021 by David Gerber - https://zapek.com + * + * This file is part of Xeres. + * + * Xeres is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Xeres is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Xeres. If not, see . + */ + +package io.xeres.app.xrs.item; + +import org.junit.jupiter.api.Test; + +import static io.xeres.app.xrs.item.ItemPriority.*; +import static org.junit.jupiter.api.Assertions.assertEquals; + +class ItemPriorityTest +{ + @Test + void ItemPriority_Enum_Value() + { + assertEquals(3, DEFAULT.getPriority()); + assertEquals(6, GXS.getPriority()); + assertEquals(7, CHAT.getPriority()); + } +} diff --git a/app/src/test/java/io/xeres/app/xrs/serialization/SerialAll.java b/app/src/test/java/io/xeres/app/xrs/serialization/SerialAll.java new file mode 100644 index 000000000..dd9fa41c4 --- /dev/null +++ b/app/src/test/java/io/xeres/app/xrs/serialization/SerialAll.java @@ -0,0 +1,328 @@ +/* + * Copyright (c) 2019-2021 by David Gerber - https://zapek.com + * + * This file is part of Xeres. + * + * Xeres is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Xeres is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Xeres. If not, see . + */ + +package io.xeres.app.xrs.serialization; + +import io.xeres.common.id.LocationId; + +import java.util.EnumSet; +import java.util.List; +import java.util.Map; + +public class SerialAll +{ + @RsSerialized + private int intPrimitiveField; + + @RsSerialized + private Integer integerField; + + @RsSerialized + private short shortPrimitiveField; + + @RsSerialized + private Short shortField; + + @RsSerialized + private byte bytePrimitiveField; + + @RsSerialized + private Byte byteField; + + @RsSerialized + private long longPrimitiveField; + + @RsSerialized + private Long longField; + + @RsSerialized + private float floatPrimitiveField; + + @RsSerialized + private Float floatField; + + @RsSerialized + private double doublePrimitiveField; + + @RsSerialized + private Double doubleField; + + @RsSerialized + private boolean booleanPrimitiveField; + + @RsSerialized + private Boolean booleanField; + + @RsSerialized + private byte[] bytes; + + @RsSerialized + private LocationId locationId; + + @RsSerialized + private List stringList; + + @RsSerialized + private Map stringMap; + + @RsSerialized + private SerialEnum serialEnum; + + @RsSerialized + private EnumSet enumSet; + + @RsSerialized(fieldSize = FieldSize.SHORT) + private EnumSet enumSetShort; + + @RsSerialized(fieldSize = FieldSize.BYTE) + private EnumSet enumSetByte; + + @RsSerialized(tlvType = TlvType.STR_NAME) + private String tlvName; + + public int getIntPrimitiveField() + { + return intPrimitiveField; + } + + public void setIntPrimitiveField(int intPrimitiveField) + { + this.intPrimitiveField = intPrimitiveField; + } + + public Integer getIntegerField() + { + return integerField; + } + + public void setIntegerField(Integer integerField) + { + this.integerField = integerField; + } + + public short getShortPrimitiveField() + { + return shortPrimitiveField; + } + + public void setShortPrimitiveField(short shortPrimitiveField) + { + this.shortPrimitiveField = shortPrimitiveField; + } + + public Short getShortField() + { + return shortField; + } + + public void setShortField(Short shortField) + { + this.shortField = shortField; + } + + public byte getBytePrimitiveField() + { + return bytePrimitiveField; + } + + public void setBytePrimitiveField(byte bytePrimitiveField) + { + this.bytePrimitiveField = bytePrimitiveField; + } + + public Byte getByteField() + { + return byteField; + } + + public void setByteField(Byte byteField) + { + this.byteField = byteField; + } + + public long getLongPrimitiveField() + { + return longPrimitiveField; + } + + public void setLongPrimitiveField(long longPrimitiveField) + { + this.longPrimitiveField = longPrimitiveField; + } + + public Long getLongField() + { + return longField; + } + + public void setLongField(Long longField) + { + this.longField = longField; + } + + public float getFloatPrimitiveField() + { + return floatPrimitiveField; + } + + public void setFloatPrimitiveField(float floatPrimitiveField) + { + this.floatPrimitiveField = floatPrimitiveField; + } + + public Float getFloatField() + { + return floatField; + } + + public void setFloatField(Float floatField) + { + this.floatField = floatField; + } + + public double getDoublePrimitiveField() + { + return doublePrimitiveField; + } + + public void setDoublePrimitiveField(double doublePrimitiveField) + { + this.doublePrimitiveField = doublePrimitiveField; + } + + public Double getDoubleField() + { + return doubleField; + } + + public void setDoubleField(Double doubleField) + { + this.doubleField = doubleField; + } + + public boolean isBooleanPrimitiveField() + { + return booleanPrimitiveField; + } + + public void setBooleanPrimitiveField(boolean booleanPrimitiveField) + { + this.booleanPrimitiveField = booleanPrimitiveField; + } + + public Boolean getBooleanField() + { + return booleanField; + } + + public void setBooleanField(Boolean booleanField) + { + this.booleanField = booleanField; + } + + public byte[] getBytes() + { + return bytes; + } + + public void setBytes(byte[] bytes) + { + this.bytes = bytes; + } + + public LocationId getLocationId() + { + return locationId; + } + + public void setLocationId(LocationId locationId) + { + this.locationId = locationId; + } + + public List getStringList() + { + return stringList; + } + + public void setStringList(List stringList) + { + this.stringList = stringList; + } + + public Map getStringMap() + { + return stringMap; + } + + public void setStringMap(Map stringMap) + { + this.stringMap = stringMap; + } + + public SerialEnum getSerialEnum() + { + return serialEnum; + } + + public void setSerialEnum(SerialEnum serialEnum) + { + this.serialEnum = serialEnum; + } + + public EnumSet getEnumSet() + { + return enumSet; + } + + public void setEnumSet(EnumSet enumSet) + { + this.enumSet = enumSet; + } + + public String getTlvName() + { + return tlvName; + } + + public void setTlvName(String tlvName) + { + this.tlvName = tlvName; + } + + public EnumSet getEnumSetShort() + { + return enumSetShort; + } + + public void setEnumSetShort(EnumSet enumSetShort) + { + this.enumSetShort = enumSetShort; + } + + public EnumSet getEnumSetByte() + { + return enumSetByte; + } + + public void setEnumSetByte(EnumSet enumSetByte) + { + this.enumSetByte = enumSetByte; + } +} diff --git a/app/src/test/java/io/xeres/app/xrs/serialization/SerialEnum.java b/app/src/test/java/io/xeres/app/xrs/serialization/SerialEnum.java new file mode 100644 index 000000000..bbf4d2a3a --- /dev/null +++ b/app/src/test/java/io/xeres/app/xrs/serialization/SerialEnum.java @@ -0,0 +1,28 @@ +/* + * Copyright (c) 2019-2021 by David Gerber - https://zapek.com + * + * This file is part of Xeres. + * + * Xeres is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Xeres is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Xeres. If not, see . + */ + +package io.xeres.app.xrs.serialization; + +public enum SerialEnum +{ + ONE, + TWO, + THREE, + FOUR +} diff --git a/app/src/test/java/io/xeres/app/xrs/serialization/SerialList.java b/app/src/test/java/io/xeres/app/xrs/serialization/SerialList.java new file mode 100644 index 000000000..c290d7d8c --- /dev/null +++ b/app/src/test/java/io/xeres/app/xrs/serialization/SerialList.java @@ -0,0 +1,33 @@ +/* + * Copyright (c) 2019-2021 by David Gerber - https://zapek.com + * + * This file is part of Xeres. + * + * Xeres is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Xeres is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Xeres. If not, see . + */ + +package io.xeres.app.xrs.serialization; + +import java.util.List; + +public class SerialList +{ + @RsSerialized + private List list; + + public List getList() + { + return list; + } +} diff --git a/app/src/test/java/io/xeres/app/xrs/serialization/SerialMap.java b/app/src/test/java/io/xeres/app/xrs/serialization/SerialMap.java new file mode 100644 index 000000000..eb348099a --- /dev/null +++ b/app/src/test/java/io/xeres/app/xrs/serialization/SerialMap.java @@ -0,0 +1,33 @@ +/* + * Copyright (c) 2019-2021 by David Gerber - https://zapek.com + * + * This file is part of Xeres. + * + * Xeres is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Xeres is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Xeres. If not, see . + */ + +package io.xeres.app.xrs.serialization; + +import java.util.Map; + +public class SerialMap +{ + @RsSerialized + private Map map; + + public Map getMap() + { + return map; + } +} diff --git a/app/src/test/java/io/xeres/app/xrs/serialization/SerializerTest.java b/app/src/test/java/io/xeres/app/xrs/serialization/SerializerTest.java new file mode 100644 index 000000000..a0da05215 --- /dev/null +++ b/app/src/test/java/io/xeres/app/xrs/serialization/SerializerTest.java @@ -0,0 +1,573 @@ +/* + * Copyright (c) 2019-2021 by David Gerber - https://zapek.com + * + * This file is part of Xeres. + * + * Xeres is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Xeres is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Xeres. If not, see . + */ + +package io.xeres.app.xrs.serialization; + +import io.netty.buffer.Unpooled; +import io.xeres.app.database.model.location.LocationFakes; +import io.xeres.app.xrs.common.Signature; +import io.xeres.app.xrs.common.SignatureSet; +import io.xeres.common.id.GxsId; +import io.xeres.common.id.LocationId; +import org.apache.commons.lang3.RandomUtils; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; + +import java.util.EnumSet; +import java.util.List; +import java.util.Map; + +import static io.xeres.app.xrs.serialization.Serializer.TLV_HEADER_SIZE; +import static org.junit.jupiter.api.Assertions.*; + +class SerializerTest +{ + @ParameterizedTest + @ValueSource(ints = {Integer.MIN_VALUE, Integer.MAX_VALUE, 0, 5}) + void Serializer_Serialize_Int(int input) + { + var buf = Unpooled.buffer(); + + int size = Serializer.serialize(buf, input); + + assertEquals(4, size); + assertEquals(input, buf.getInt(0)); + + int result = Serializer.deserializeInt(buf); + assertEquals(input, result); + buf.release(); + } + + @Test + void Serializer_Serialize_Int_Null() + { + var buf = Unpooled.buffer(); + + assertThrows(NullPointerException.class, () -> Serializer.serialize(buf, (Integer) null)); + buf.release(); + } + + @ParameterizedTest + @ValueSource(shorts = {Short.MIN_VALUE, Short.MAX_VALUE, 0, 5}) + void Serializer_Serialize_Short(short input) + { + var buf = Unpooled.buffer(); + + int size = Serializer.serialize(buf, input); + + assertEquals(2, size); + assertEquals(input, buf.getShort(0)); + + short result = Serializer.deserializeShort(buf); + assertEquals(input, result); + buf.release(); + } + + @Test + void Serializer_Serialize_Short_Null() + { + var buf = Unpooled.buffer(); + + assertThrows(NullPointerException.class, () -> Serializer.serialize(buf, (Short) null)); + buf.release(); + } + + @ParameterizedTest + @ValueSource(bytes = {Byte.MIN_VALUE, Byte.MAX_VALUE, 0, 5}) + void Serializer_Serialize_Byte(byte input) + { + var buf = Unpooled.buffer(); + + int size = Serializer.serialize(buf, input); + + assertEquals(1, size); + assertEquals(input, buf.getByte(0)); + + byte result = Serializer.deserializeByte(buf); + assertEquals(input, result); + buf.release(); + } + + @Test + void Serializer_Serialize_Byte_Null() + { + var buf = Unpooled.buffer(); + + assertThrows(NullPointerException.class, () -> Serializer.serialize(buf, (Byte) null)); + buf.release(); + } + + @ParameterizedTest + @ValueSource(longs = {Long.MIN_VALUE, Long.MAX_VALUE, 0L, 5L}) + void Serializer_Serialize_Long(long input) + { + var buf = Unpooled.buffer(); + + int size = Serializer.serialize(buf, input); + + assertEquals(8, size); + assertEquals(input, buf.getLong(0)); + + long result = Serializer.deserializeLong(buf); + assertEquals(input, result); + buf.release(); + } + + @Test + void Serializer_Serialize_Long_Null() + { + var buf = Unpooled.buffer(); + + assertThrows(NullPointerException.class, () -> Serializer.serialize(buf, (Long) null)); + buf.release(); + } + + @ParameterizedTest + @ValueSource(floats = {Float.MIN_VALUE, Float.MAX_VALUE, 0f, 5f}) + void Serializer_Serialize_Float(float input) + { + var buf = Unpooled.buffer(); + + int size = Serializer.serialize(buf, input); + + assertEquals(4, size); + assertEquals(input, buf.getFloat(0)); + + float result = Serializer.deserializeFloat(buf); + assertEquals(input, result); + buf.release(); + } + + @Test + void Serializer_Serialize_Float_Null() + { + var buf = Unpooled.buffer(); + + assertThrows(NullPointerException.class, () -> Serializer.serialize(buf, (Float) null)); + buf.release(); + } + + @ParameterizedTest + @ValueSource(doubles = {Double.MIN_VALUE, Double.MAX_VALUE, 0.0, 5.0}) + void Serializer_Serialize_Double(double input) + { + var buf = Unpooled.buffer(); + + int size = Serializer.serialize(buf, input); + + assertEquals(8, size); + assertEquals(input, buf.getDouble(0)); + + double result = Serializer.deserializeDouble(buf); + assertEquals(input, result); + buf.release(); + } + + @Test + void Serializer_Serialize_Double_Null() + { + var buf = Unpooled.buffer(); + + assertThrows(NullPointerException.class, () -> Serializer.serialize(buf, (Double) null)); + buf.release(); + } + + @ParameterizedTest + @ValueSource(booleans = {true, false}) + void Serializer_Serialize_Boolean(boolean input) + { + var buf = Unpooled.buffer(); + + int size = Serializer.serialize(buf, input); + + assertEquals(1, size); + assertEquals(input, buf.getBoolean(0)); + + boolean result = Serializer.deserializeBoolean(buf); + assertEquals(input, result); + buf.release(); + } + + @Test + void Serializer_Serialize_Boolean_Null() + { + var buf = Unpooled.buffer(); + + assertThrows(NullPointerException.class, () -> Serializer.serialize(buf, (Boolean) null)); + buf.release(); + } + + @ParameterizedTest + @ValueSource(strings = {"", "hello", "hello world", " "}) + void Serializer_Serialize_String(String input) + { + var buf = Unpooled.buffer(); + + int size = Serializer.serialize(buf, input); + + byte[] stringBytes = input.getBytes(); + + assertEquals(stringBytes.length + 4, size); + byte[] output = new byte[stringBytes.length]; + buf.getBytes(4, output); + assertArrayEquals(stringBytes, output); + + String result = Serializer.deserializeString(buf); + assertEquals(input, result); + buf.release(); + } + + @Test + void Serializer_Serialize_String_Null() + { + var buf = Unpooled.buffer(); + + int size = Serializer.serialize(buf, (String) null); + assertEquals(4, size); + buf.release(); + } + + @Test + void Serializer_Serialize_ByteArray() + { + var buf = Unpooled.buffer(); + + var input = new byte[]{1, 2, 3}; + + int size = Serializer.serialize(buf, input); + + assertEquals(4 + input.length, size); + byte[] output = new byte[input.length]; + buf.getBytes(4, output); + assertArrayEquals(input, output); + + var result = Serializer.deserializeByteArray(buf); + assertArrayEquals(input, result); + buf.release(); + } + + @Test + void Serializer_Serialize_ByteArray_Null() + { + var buf = Unpooled.buffer(); + + int size = Serializer.serialize(buf, (byte[]) null); + assertEquals(4, size); + buf.release(); + } + + @Test + void Serializer_Serialize_Identifier() + { + var buf = Unpooled.buffer(); + + var input = LocationFakes.createLocation().getLocationId(); + + int size = Serializer.serialize(buf, input); + + assertEquals(input.getLength(), size); + byte[] output = new byte[input.getLength()]; + buf.getBytes(0, output); + assertArrayEquals(input.getBytes(), output); + + var result = (LocationId) Serializer.deserialize(buf, LocationId.class); + + assertEquals(input, result); + buf.release(); + } + + @Test + void Serializer_Serialize_Identifier_Null() + { + var buf = Unpooled.buffer(); + + int size = IdentifierSerializer.serialize(buf, GxsId.class, null); + assertEquals(GxsId.LENGTH, size); + + var result = (GxsId) Serializer.deserialize(buf, GxsId.class); + assertNull(result); + buf.release(); + } + + @Test + void Serializer_Serialize_List() + { + var buf = Unpooled.buffer(); + + var input = List.of("hello", "dude"); + + int size = Serializer.serialize(buf, input.getClass(), input, null); + + var listObject = new SerialList(); + var result = Serializer.deserializeAnnotatedFields(buf, listObject); + assertTrue(result); + assertEquals(input.size(), listObject.getList().size()); + assertArrayEquals(input.get(0).getBytes(), listObject.getList().get(0).getBytes()); + assertArrayEquals(input.get(1).getBytes(), listObject.getList().get(1).getBytes()); + + assertEquals(4 + 4 + input.get(0).getBytes().length + 4 + input.get(1).getBytes().length, size); + assertEquals(input.size(), buf.getInt(0)); + buf.release(); + } + + @Test + void Serializer_Serialize_List_Null() + { + var buf = Unpooled.buffer(); + + int size = Serializer.serialize(buf, List.class, null, null); + assertEquals(4, size); + + buf.release(); + } + + @Test + void Serializer_Serialize_Map() + { + var buf = Unpooled.buffer(); + + var input = Map.of(1, "foo", 2, "barbaz"); + + int size = Serializer.serialize(buf, input.getClass(), input, null); + + var mapObject = new SerialMap(); + var result = Serializer.deserializeAnnotatedFields(buf, mapObject); + assertTrue(result); + assertEquals(input.size(), mapObject.getMap().size()); + assertArrayEquals(input.get(1).getBytes(), mapObject.getMap().get(1).getBytes()); + assertArrayEquals(input.get(2).getBytes(), mapObject.getMap().get(2).getBytes()); + + assertEquals(67, size); + buf.release(); + } + + @Test + void Serializer_Serialize_Map_Null() + { + var buf = Unpooled.buffer(); + + int size = Serializer.serialize(buf, Map.class, null, null); + assertEquals(6, size); + + buf.release(); + } + + @Test + void Serializer_Serialize_Enum() + { + var buf = Unpooled.buffer(); + + var input = SerialEnum.TWO; + + int size = Serializer.serialize(buf, input); + assertEquals(4, size); + assertEquals(1, buf.getInt(0)); + + var result = Serializer.deserializeEnum(buf, SerialEnum.class); + assertEquals(input, result); + + buf.release(); + } + + @Test + void Serializer_Serialize_Enum_Null() + { + var buf = Unpooled.buffer(); + + assertThrows(NullPointerException.class, () -> Serializer.serialize(buf, (Enum) null)); + buf.release(); + } + + @Test + void Serializer_Serialize_EnumSet() + { + var buf = Unpooled.buffer(); + + var input = EnumSet.of(SerialEnum.TWO, SerialEnum.FOUR); + + int size = Serializer.serialize(buf, input, FieldSize.INTEGER); + assertEquals(4, size); + assertEquals(1 << 1 | 1 << 3, buf.getInt(0)); + + var result = Serializer.deserializeEnumSet(buf, SerialEnum.class, FieldSize.INTEGER); + assertEquals(input, result); + + buf.release(); + } + + @Test + void Serializer_Serialize_EnumSet_Null() + { + var buf = Unpooled.buffer(); + + assertThrows(NullPointerException.class, () -> Serializer.serialize(buf, (EnumSet) null, FieldSize.INTEGER)); + buf.release(); + } + + @Test + void Serializer_Serialize_TlvString() + { + var buf = Unpooled.buffer(); + + var input = "foobar"; + + int size = Serializer.serialize(buf, TlvType.STR_NAME, input); + assertEquals(6 + input.getBytes().length, size); + + var result = Serializer.deserialize(buf, TlvType.STR_NAME); + assertEquals(input, result); + + buf.release(); + } + + @Test + void Serializer_Serialize_TlvKeySignature() + { + var buf = Unpooled.buffer(); + var key = new byte[1]; + + var input = new Signature(new GxsId(RandomUtils.nextBytes(16)), key); + + int size = Serializer.serialize(buf, TlvType.SIGNATURE, input); + assertEquals(6 + 6 + 38 + key.length, size); + + var result = (Signature) Serializer.deserialize(buf, TlvType.SIGNATURE); + assertEquals(input.getGxsId(), result.getGxsId()); + assertArrayEquals(input.getData(), result.getData()); + + buf.release(); + } + + @Test + void Serializer_Serialize_TlvKeySignatureSet() + { + var buf = Unpooled.buffer(); + var input = new SignatureSet(); + var gxsId = new GxsId(RandomUtils.nextBytes(16)); + var signature = RandomUtils.nextBytes(20); + var keySignature = new Signature(gxsId, signature); + input.put(SignatureSet.Type.ADMIN, new Signature(gxsId, signature)); + + int size = Serializer.serialize(buf, TlvType.SIGNATURE_SET, input); + assertEquals(TLV_HEADER_SIZE + TLV_HEADER_SIZE + 4 + TLV_HEADER_SIZE + TLV_HEADER_SIZE + GxsId.LENGTH * 2 + TLV_HEADER_SIZE + signature.length, size); + + var result = (SignatureSet) Serializer.deserialize(buf, TlvType.SIGNATURE_SET); + assertEquals(input.getSignatures().get(SignatureSet.Type.ADMIN.getValue()).getGxsId(), result.getSignatures().get(SignatureSet.Type.ADMIN.getValue()).getGxsId()); + assertArrayEquals(input.getSignatures().get(SignatureSet.Type.ADMIN.getValue()).getData(), result.getSignatures().get(SignatureSet.Type.ADMIN.getValue()).getData()); + + buf.release(); + } + + @Test + void Serializer_Serialize_ComplexObject() + { + var buf = Unpooled.buffer(); + + var input = new SerialAll(); + + input.setIntPrimitiveField(5); + input.setIntegerField(5); + + input.setShortPrimitiveField((short) 8); + input.setShortField((short) 8); + + input.setBytePrimitiveField((byte) 10); + input.setByteField((byte) 10); + + input.setLongPrimitiveField(12L); + input.setLongField(12L); + + input.setFloatPrimitiveField(14f); + input.setFloatField(14f); + + input.setDoublePrimitiveField(16.0); + input.setDoubleField(16.0); + + input.setBooleanPrimitiveField(true); + input.setBooleanField(true); + + input.setBytes(new byte[]{1, 2, 3}); + + input.setLocationId(LocationFakes.createLocation().getLocationId()); + + input.setStringList(List.of("foo", "bar")); + + input.setStringMap(Map.of(1, "bleh", 2, "plop")); + + input.setSerialEnum(SerialEnum.THREE); + + input.setEnumSet(EnumSet.of(SerialEnum.ONE, SerialEnum.TWO)); + + input.setEnumSetByte(EnumSet.of(SerialEnum.ONE, SerialEnum.TWO)); + + input.setEnumSetShort(EnumSet.of(SerialEnum.ONE, SerialEnum.TWO)); + + input.setTlvName("foobar"); + + int size = Serializer.serialize(buf, input.getClass(), input, null); + assertTrue(size > 0); + + SerialAll result = (SerialAll) Serializer.deserialize(buf, SerialAll.class); + + assertEquals(input.getIntPrimitiveField(), result.getIntPrimitiveField()); + assertEquals(input.getIntegerField(), result.getIntegerField()); + + assertEquals(input.getShortPrimitiveField(), result.getShortPrimitiveField()); + assertEquals(input.getShortField(), result.getShortField()); + + assertEquals(input.getBytePrimitiveField(), result.getBytePrimitiveField()); + assertEquals(input.getByteField(), result.getByteField()); + + assertEquals(input.getLongPrimitiveField(), result.getLongPrimitiveField()); + assertEquals(input.getLongField(), result.getLongField()); + + assertEquals(input.getFloatPrimitiveField(), result.getFloatPrimitiveField()); + assertEquals(input.getFloatField(), result.getFloatField()); + + assertEquals(input.getDoublePrimitiveField(), result.getDoublePrimitiveField()); + assertEquals(input.getDoubleField(), result.getDoubleField()); + + assertEquals(input.isBooleanPrimitiveField(), result.isBooleanPrimitiveField()); + assertEquals(input.getBooleanField(), result.getBooleanField()); + + assertArrayEquals(input.getBytes(), result.getBytes()); + + assertEquals(input.getLocationId().getLength(), result.getLocationId().getLength()); + assertArrayEquals(input.getLocationId().getBytes(), result.getLocationId().getBytes()); + + assertEquals(input.getStringList().size(), result.getStringList().size()); + assertIterableEquals(input.getStringList(), result.getStringList()); + + assertEquals(input.getStringMap().size(), result.getStringMap().size()); + assertEquals(input.getStringMap(), result.getStringMap()); + + assertEquals(input.getSerialEnum(), result.getSerialEnum()); + + assertEquals(input.getEnumSet(), result.getEnumSet()); + + assertEquals(input.getEnumSetByte(), result.getEnumSetByte()); + + assertEquals(input.getEnumSetShort(), result.getEnumSetShort()); + + assertEquals(input.getTlvName(), result.getTlvName()); + + buf.release(); + } +} diff --git a/app/src/test/java/io/xeres/app/xrs/serialization/TlvTypeTest.java b/app/src/test/java/io/xeres/app/xrs/serialization/TlvTypeTest.java new file mode 100644 index 000000000..d7a4b516e --- /dev/null +++ b/app/src/test/java/io/xeres/app/xrs/serialization/TlvTypeTest.java @@ -0,0 +1,54 @@ +/* + * Copyright (c) 2019-2021 by David Gerber - https://zapek.com + * + * This file is part of Xeres. + * + * Xeres is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Xeres is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Xeres. If not, see . + */ + +package io.xeres.app.xrs.serialization; + +import org.junit.jupiter.api.Test; + +import static io.xeres.app.xrs.serialization.TlvType.*; +import static org.junit.jupiter.api.Assertions.assertEquals; + +class TlvTypeTest +{ + @Test + void TlvType_Enum_Value() + { + assertEquals(0, NONE.getValue()); + assertEquals(0x51, STR_NAME.getValue()); + assertEquals(0x57, STR_MSG.getValue()); + assertEquals(0x5a, STR_GENID.getValue()); + assertEquals(0x5c, STR_LOCATION.getValue()); + assertEquals(0x5f, STR_VERSION.getValue()); + assertEquals(0x70, STR_HASH_SHA1.getValue()); + assertEquals(0x83, STR_DYNDNS.getValue()); + assertEquals(0x84, STR_DOM_ADDR.getValue()); + assertEquals(0x85, IPV4.getValue()); + assertEquals(0x86, IPV6.getValue()); + assertEquals(0xa4, STR_KEY_ID.getValue()); + assertEquals(0xb4, STR_SIGN.getValue()); + assertEquals(0x120, SIGN_RSA_SHA1.getValue()); + assertEquals(0x1023, SET_PGP_ID.getValue()); + assertEquals(0x1024, SET_RECOGN.getValue()); + assertEquals(0x1050, SIGNATURE.getValue()); + assertEquals(0x1070, ADDRESS_INFO.getValue()); + assertEquals(0x1071, ADDRESS_SET.getValue()); + assertEquals(0x1072, ADDRESS.getValue()); + assertEquals(0x2223, STRING.getValue()); + } +} diff --git a/app/src/test/java/io/xeres/app/xrs/serialization/TlvUtilsTest.java b/app/src/test/java/io/xeres/app/xrs/serialization/TlvUtilsTest.java new file mode 100644 index 000000000..99ec04034 --- /dev/null +++ b/app/src/test/java/io/xeres/app/xrs/serialization/TlvUtilsTest.java @@ -0,0 +1,32 @@ +/* + * Copyright (c) 2019-2021 by David Gerber - https://zapek.com + * + * This file is part of Xeres. + * + * Xeres is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Xeres is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Xeres. If not, see . + */ + +package io.xeres.app.xrs.serialization; + +import io.xeres.testutils.TestUtils; +import org.junit.jupiter.api.Test; + +class TlvUtilsTest +{ + @Test + void TlvUtils_NoInstance_OK() throws NoSuchMethodException + { + TestUtils.assertUtilityClass(TlvUtils.class); + } +} diff --git a/app/src/test/java/io/xeres/app/xrs/service/RsServiceRegistryTest.java b/app/src/test/java/io/xeres/app/xrs/service/RsServiceRegistryTest.java new file mode 100644 index 000000000..4debb3d40 --- /dev/null +++ b/app/src/test/java/io/xeres/app/xrs/service/RsServiceRegistryTest.java @@ -0,0 +1,32 @@ +/* + * Copyright (c) 2019-2021 by David Gerber - https://zapek.com + * + * This file is part of Xeres. + * + * Xeres is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Xeres is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Xeres. If not, see . + */ + +package io.xeres.app.xrs.service; + +import io.xeres.testutils.TestUtils; +import org.junit.jupiter.api.Test; + +class RsServiceRegistryTest +{ + @Test + void RsServiceRegistry_NoInstance_OK() throws NoSuchMethodException + { + TestUtils.assertUtilityClass(RsServiceRegistry.class); + } +} diff --git a/app/src/test/java/io/xeres/app/xrs/service/chat/ChatFlagsTest.java b/app/src/test/java/io/xeres/app/xrs/service/chat/ChatFlagsTest.java new file mode 100644 index 000000000..4744fe2e1 --- /dev/null +++ b/app/src/test/java/io/xeres/app/xrs/service/chat/ChatFlagsTest.java @@ -0,0 +1,47 @@ +/* + * Copyright (c) 2019-2021 by David Gerber - https://zapek.com + * + * This file is part of Xeres. + * + * Xeres is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Xeres is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Xeres. If not, see . + */ + +package io.xeres.app.xrs.service.chat; + +import org.junit.jupiter.api.Test; + +import static io.xeres.app.xrs.service.chat.ChatFlags.*; +import static org.junit.jupiter.api.Assertions.assertEquals; + +class ChatFlagsTest +{ + @Test + void ChatFlags_Enum_Order() + { + assertEquals(0, PRIVATE.ordinal()); + assertEquals(1, REQUEST_AVATAR.ordinal()); + assertEquals(2, CONTAINS_AVATAR.ordinal()); + assertEquals(3, AVATAR_AVAILABLE.ordinal()); + assertEquals(4, CUSTOM_STATE.ordinal()); + assertEquals(5, PUBLIC.ordinal()); + assertEquals(6, REQUEST_CUSTOM_STATE.ordinal()); + assertEquals(7, CUSTOM_STATE_AVAILABLE.ordinal()); + assertEquals(8, PARTIAL_MESSAGE.ordinal()); + assertEquals(9, LOBBY.ordinal()); + assertEquals(10, CLOSING_DISTANT_CONNECTION.ordinal()); + assertEquals(11, ACK_DISTANT_CONNECTION.ordinal()); + assertEquals(12, KEEP_ALIVE.ordinal()); + assertEquals(13, CONNECTION_REFUSED.ordinal()); + } +} diff --git a/app/src/test/java/io/xeres/app/xrs/service/chat/ChatRoomEventTest.java b/app/src/test/java/io/xeres/app/xrs/service/chat/ChatRoomEventTest.java new file mode 100644 index 000000000..e2f9e0e5b --- /dev/null +++ b/app/src/test/java/io/xeres/app/xrs/service/chat/ChatRoomEventTest.java @@ -0,0 +1,38 @@ +/* + * Copyright (c) 2019-2021 by David Gerber - https://zapek.com + * + * This file is part of Xeres. + * + * Xeres is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Xeres is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Xeres. If not, see . + */ + +package io.xeres.app.xrs.service.chat; + +import org.junit.jupiter.api.Test; + +import static io.xeres.app.xrs.service.chat.item.ChatRoomEvent.*; +import static org.junit.jupiter.api.Assertions.assertEquals; + +class ChatRoomEventTest +{ + @Test + void ChatRoomEventTest_Enum_Values() + { + assertEquals(1, PEER_LEFT.getCode()); + assertEquals(2, PEER_STATUS.getCode()); + assertEquals(3, PEER_JOINED.getCode()); + assertEquals(4, PEER_CHANGE_NICKNAME.getCode()); + assertEquals(5, KEEP_ALIVE.getCode()); + } +} diff --git a/app/src/test/java/io/xeres/app/xrs/service/chat/ChatServiceTest.java b/app/src/test/java/io/xeres/app/xrs/service/chat/ChatServiceTest.java new file mode 100644 index 000000000..50fbdcfdb --- /dev/null +++ b/app/src/test/java/io/xeres/app/xrs/service/chat/ChatServiceTest.java @@ -0,0 +1,63 @@ +/* + * Copyright (c) 2019-2021 by David Gerber - https://zapek.com + * + * This file is part of Xeres. + * + * Xeres is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Xeres is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Xeres. If not, see . + */ + +package io.xeres.app.xrs.service.chat; + +import io.xeres.app.database.model.location.LocationFakes; +import io.xeres.app.net.peer.PeerConnection; +import io.xeres.app.net.peer.PeerConnectionManager; +import io.xeres.app.xrs.service.chat.item.ChatMessageItem; +import io.xeres.common.message.MessageType; +import io.xeres.common.message.chat.PrivateChatMessage; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.springframework.test.context.junit.jupiter.SpringExtension; + +import java.util.EnumSet; + +import static io.xeres.common.rest.PathConfig.CHAT_PATH; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.verify; + +@ExtendWith(SpringExtension.class) +class ChatServiceTest +{ + @Mock + private PeerConnectionManager peerConnectionManager; + + @InjectMocks + private ChatService chatService; + + // Unfortunately, only simple stuff can be tested. The rest requires mocking a lot of stuff (identities, keys, etc...) + + @Test + void ChatService_HandleChatMessageItem_OK() + { + var MESSAGE = "hello"; + var peerConnection = new PeerConnection(LocationFakes.createLocation(), null); + + var item = new ChatMessageItem(MESSAGE, EnumSet.of(ChatFlags.PRIVATE)); + chatService.handleItem(peerConnection, item); + + verify(peerConnectionManager).sendToSubscriptions(eq(CHAT_PATH), eq(MessageType.CHAT_PRIVATE_MESSAGE), eq(peerConnection.getLocation().getLocationId()), any(PrivateChatMessage.class)); + } +} diff --git a/app/src/test/java/io/xeres/app/xrs/service/chat/RoomFlagsTest.java b/app/src/test/java/io/xeres/app/xrs/service/chat/RoomFlagsTest.java new file mode 100644 index 000000000..d51156d1c --- /dev/null +++ b/app/src/test/java/io/xeres/app/xrs/service/chat/RoomFlagsTest.java @@ -0,0 +1,38 @@ +/* + * Copyright (c) 2019-2021 by David Gerber - https://zapek.com + * + * This file is part of Xeres. + * + * Xeres is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Xeres is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Xeres. If not, see . + */ + +package io.xeres.app.xrs.service.chat; + +import org.junit.jupiter.api.Test; + +import static io.xeres.app.xrs.service.chat.RoomFlags.*; +import static org.junit.jupiter.api.Assertions.assertEquals; + +class RoomFlagsTest +{ + @Test + void RoomFlags_Enum_Order() + { + assertEquals(0, AUTO_SUBSCRIBE.ordinal()); + assertEquals(1, DEPRECATED.ordinal()); + assertEquals(2, PUBLIC.ordinal()); + assertEquals(3, CHALLENGE.ordinal()); + assertEquals(4, PGP_SIGNED.ordinal()); + } +} diff --git a/app/src/test/java/io/xeres/app/xrs/service/discovery/DiscoveryPgpListItemTest.java b/app/src/test/java/io/xeres/app/xrs/service/discovery/DiscoveryPgpListItemTest.java new file mode 100644 index 000000000..6b8797454 --- /dev/null +++ b/app/src/test/java/io/xeres/app/xrs/service/discovery/DiscoveryPgpListItemTest.java @@ -0,0 +1,36 @@ +/* + * Copyright (c) 2019-2021 by David Gerber - https://zapek.com + * + * This file is part of Xeres. + * + * Xeres is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Xeres is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Xeres. If not, see . + */ + +package io.xeres.app.xrs.service.discovery; + +import io.xeres.app.xrs.service.discovery.item.DiscoveryPgpListItem.Mode; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +class DiscoveryPgpListItemTest +{ + @Test + void DiscoveryPgpListItem_Mode_Enum_Order() + { + assertEquals(0, Mode.NONE.ordinal()); + assertEquals(1, Mode.FRIENDS.ordinal()); + assertEquals(2, Mode.GET_CERT.ordinal()); + } +} diff --git a/app/src/test/java/io/xeres/app/xrs/service/discovery/DiscoveryServiceTest.java b/app/src/test/java/io/xeres/app/xrs/service/discovery/DiscoveryServiceTest.java new file mode 100644 index 000000000..3f9429549 --- /dev/null +++ b/app/src/test/java/io/xeres/app/xrs/service/discovery/DiscoveryServiceTest.java @@ -0,0 +1,250 @@ +/* + * Copyright (c) 2019-2021 by David Gerber - https://zapek.com + * + * This file is part of Xeres. + * + * Xeres is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Xeres is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Xeres. If not, see . + */ + +package io.xeres.app.xrs.service.discovery; + +import io.xeres.app.database.model.location.Location; +import io.xeres.app.database.model.location.LocationFakes; +import io.xeres.app.database.model.profile.ProfileFakes; +import io.xeres.app.net.peer.PeerConnection; +import io.xeres.app.net.peer.PeerConnectionManager; +import io.xeres.app.service.LocationService; +import io.xeres.app.service.ProfileService; +import io.xeres.app.xrs.item.Item; +import io.xeres.app.xrs.service.RsService; +import io.xeres.app.xrs.service.discovery.item.DiscoveryContactItem; +import io.xeres.app.xrs.service.discovery.item.DiscoveryPgpListItem; +import io.xeres.common.id.LocationId; +import io.xeres.common.protocol.NetMode; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.ArgumentCaptor; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.springframework.test.context.junit.jupiter.SpringExtension; + +import java.util.List; +import java.util.Optional; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.Mockito.*; + +@ExtendWith(SpringExtension.class) +class DiscoveryServiceTest +{ + @Mock + private PeerConnectionManager peerConnectionManager; + + @Mock + private ProfileService profileService; + + @Mock + private LocationService locationService; + + @InjectMocks + private DiscoveryService discoveryService; + + /** + * This is a case that is handled by RS but that I think is never actually sent. + * We ignore it, just in case. + */ + @Test + void DiscoveryService_handleDiscoveryContactItem_NewLocation_FriendOfFriend_Known_Ignore() + { + var peerConnection = new PeerConnection(LocationFakes.createLocation(), null); + + when(locationService.findLocationById(any(LocationId.class))).thenReturn(Optional.empty()); + when(profileService.findProfileByPgpIdentifier(anyLong())).thenReturn(Optional.empty()); + + discoveryService.handleItem(peerConnection, createDiscoveryContact(LocationFakes.createLocation())); + + verify(peerConnectionManager, times(0)).writeItem(eq(peerConnection), any(Item.class), any(RsService.class)); + } + + /** + * This is a case that shouldn't happen either. + */ + @Test + void DiscoveryService_handleDiscoveryContactItem_NewLocation_FriendOfFriend_Unknown_Ignore() + { + var peerConnection = new PeerConnection(LocationFakes.createLocation(), null); + var profile = ProfileFakes.createProfile(); + + when(locationService.findLocationById(any(LocationId.class))).thenReturn(Optional.empty()); + when(profileService.findProfileByPgpIdentifier(anyLong())).thenReturn(Optional.of(ProfileFakes.createProfile())); + + discoveryService.handleItem(peerConnection, createDiscoveryContact(LocationFakes.createLocation())); + + verify(peerConnectionManager, times(0)).writeItem(eq(peerConnection), any(Item.class), any(RsService.class)); + } + + /** + * The peer sends the new location of a common friend. We keep that new location. + */ + @Test + void DiscoveryService_handleDiscoveryContactItem_NewLocation_Friend_OK() + { + var peerLocation = LocationFakes.createLocation(); + var peerConnection = new PeerConnection(peerLocation, null); + var profile = ProfileFakes.createProfile(); + profile.setAccepted(true); + var newLocation = LocationFakes.createLocation("foo", profile); + + when(locationService.findLocationById(peerLocation.getLocationId())).thenReturn(Optional.empty()); + when(profileService.findProfileByPgpIdentifier(profile.getPgpIdentifier())).thenReturn(Optional.of(profile)); + + discoveryService.handleItem(peerConnection, createDiscoveryContact(newLocation)); + + verify(peerConnectionManager, times(0)).writeItem(eq(peerConnection), any(Item.class), any(RsService.class)); + verify(locationService).update(eq(newLocation), anyString(), any(NetMode.class), anyString(), anyBoolean(), anyBoolean(), anyList(), anyString()); + } + + /** + * The peer sends an updated location of a common friend. We update + * the location. + */ + @Test + void DiscoveryService_handleDiscoveryContactItem_UpdateLocation_Friend_OK() + { + var peerLocation = LocationFakes.createLocation(); + var peerConnection = new PeerConnection(peerLocation, null); + var profile = ProfileFakes.createProfile(); + profile.setAccepted(true); + var friendLocation = LocationFakes.createLocation("foo", profile); + + when(locationService.findLocationById(friendLocation.getLocationId())).thenReturn(Optional.of(friendLocation)); + when(locationService.findOwnLocation()).thenReturn(Optional.of(LocationFakes.createOwnLocation())); + + discoveryService.handleItem(peerConnection, createDiscoveryContact(friendLocation)); + + verify(peerConnectionManager, times(0)).writeItem(eq(peerConnection), any(Item.class), any(RsService.class)); + verify(locationService).update(eq(friendLocation), anyString(), any(NetMode.class), anyString(), anyBoolean(), anyBoolean(), anyList(), anyString()); + } + + /** + * The peer sends our own location. We do nothing (could be used to help find out our external + * IP address). + */ + @Test + void DiscoveryService_handleDiscoveryContactItem_UpdateLocation_Own_Ignore() + { + var peerLocation = LocationFakes.createLocation(); + var peerConnection = new PeerConnection(peerLocation, null); + var profile = ProfileFakes.createProfile(); + profile.setAccepted(true); + var friendLocation = LocationFakes.createLocation("foo", profile); + + when(locationService.findLocationById(friendLocation.getLocationId())).thenReturn(Optional.of(friendLocation)); + when(locationService.findOwnLocation()).thenReturn(Optional.of(friendLocation)); + + discoveryService.handleItem(peerConnection, createDiscoveryContact(friendLocation)); + + verify(peerConnectionManager, times(0)).writeItem(eq(peerConnection), any(Item.class), any(RsService.class)); + } + + /** + * The peer sends his location. We update its location and send our list + * of friends. + */ + @Test + void DiscoveryService_handleDiscoveryContactItem_UpdateLocation_Peer_OK() + { + var peerLocation = LocationFakes.createLocation(); + var peerConnection = new PeerConnection(peerLocation, null); + var ownLocation = LocationFakes.createLocation(); + var profile = ProfileFakes.createProfile(); + profile.setAccepted(true); + + when(locationService.findLocationById(peerLocation.getLocationId())).thenReturn(Optional.of(peerLocation)); + when(locationService.findOwnLocation()).thenReturn(Optional.of(ownLocation)); + when(profileService.getAllDiscoverableProfiles()).thenReturn(List.of(profile)); + + discoveryService.handleItem(peerConnection, createDiscoveryContact(peerLocation)); + + verify(locationService).update(eq(peerLocation), anyString(), any(NetMode.class), anyString(), anyBoolean(), anyBoolean(), anyList(), anyString()); + ArgumentCaptor discoveryPgpListItem = ArgumentCaptor.forClass(DiscoveryPgpListItem.class); + verify(peerConnectionManager).writeItem(eq(peerConnection), discoveryPgpListItem.capture(), any(RsService.class)); + + assertEquals(DiscoveryPgpListItem.Mode.FRIENDS, discoveryPgpListItem.getValue().getMode()); + assertTrue(discoveryPgpListItem.getValue().getPgpIds().contains(profile.getPgpIdentifier())); + } + + /** + * The peer sends his location. We update its location but don't send our list of + * friends because we're not discoverable. + */ + @Test + void DiscoveryService_handleDiscoveryContactItem_UpdateLocation_Peer_OurLocation_NotDiscoverable_OK() + { + var peerLocation = LocationFakes.createLocation(); + var peerConnection = new PeerConnection(peerLocation, null); + var ownLocation = LocationFakes.createLocation(); + ownLocation.setDiscoverable(false); + + when(locationService.findLocationById(peerLocation.getLocationId())).thenReturn(Optional.of(peerLocation)); + when(locationService.findOwnLocation()).thenReturn(Optional.of(ownLocation)); + + discoveryService.handleItem(peerConnection, createDiscoveryContact(peerLocation)); + + verify(locationService).findLocationById(eq(peerLocation.getLocationId())); + verify(locationService).findOwnLocation(); + verify(locationService).update(eq(peerLocation), anyString(), any(NetMode.class), anyString(), anyBoolean(), anyBoolean(), anyList(), anyString()); + verify(peerConnectionManager, times(0)).writeItem(eq(peerConnection), any(Item.class), any(RsService.class)); + } + + /** + * The peer sends his location. We update its location and since it's a partial profile (added through + * ShortInvites) we ask for its PGP key. + */ + @Test + void DiscoveryService_handleDiscoveryContactItem_UpdateLocation_Peer_Partial_OK() + { + var peerLocation = LocationFakes.createLocation(); + var peerConnection = new PeerConnection(peerLocation, null); + var peerProfile = ProfileFakes.createProfile(); + peerProfile.setAccepted(true); + peerProfile.setPgpPublicKeyData(null); // partial profile + peerLocation.setProfile(peerProfile); + + when(locationService.findLocationById(peerLocation.getLocationId())).thenReturn(Optional.of(peerLocation)); + + discoveryService.handleItem(peerConnection, createDiscoveryContact(peerLocation)); + + verify(locationService).update(eq(peerLocation), anyString(), any(NetMode.class), anyString(), anyBoolean(), anyBoolean(), anyList(), anyString()); + ArgumentCaptor discoveryPgpListItem = ArgumentCaptor.forClass(DiscoveryPgpListItem.class); + verify(peerConnectionManager).writeItem(eq(peerConnection), discoveryPgpListItem.capture(), any(RsService.class)); + + assertEquals(DiscoveryPgpListItem.Mode.GET_CERT, discoveryPgpListItem.getValue().getMode()); + assertTrue(discoveryPgpListItem.getValue().getPgpIds().contains(peerProfile.getPgpIdentifier())); + } + + private DiscoveryContactItem createDiscoveryContact(Location location) + { + DiscoveryContactItem.Builder builder = DiscoveryContactItem.builder(); + + builder.setPgpIdentifier(location.getProfile().getPgpIdentifier()); + builder.setLocationId(location.getLocationId()); + builder.setLocationName(location.getName()); + builder.setHostname("foobar.com"); // XXX: no hostname support in location yet + builder.setNetMode(location.getNetMode()); + builder.setVersion(location.getVersion()); + return builder.build(); + } +} diff --git a/app/src/test/java/io/xeres/app/xrs/service/gxs/GxsRequestTypeTest.java b/app/src/test/java/io/xeres/app/xrs/service/gxs/GxsRequestTypeTest.java new file mode 100644 index 000000000..91aa98952 --- /dev/null +++ b/app/src/test/java/io/xeres/app/xrs/service/gxs/GxsRequestTypeTest.java @@ -0,0 +1,36 @@ +/* + * Copyright (c) 2019-2021 by David Gerber - https://zapek.com + * + * This file is part of Xeres. + * + * Xeres is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Xeres is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Xeres. If not, see . + */ + +package io.xeres.app.xrs.service.gxs; + +import org.junit.jupiter.api.Test; + +import static io.xeres.app.xrs.service.gxs.item.RequestType.*; +import static org.junit.jupiter.api.Assertions.assertEquals; + +class GxsRequestTypeTest +{ + @Test + void RequestType_Enum_Order() + { + assertEquals(0, NONE.ordinal()); + assertEquals(1, REQUEST.ordinal()); + assertEquals(2, RESPONSE.ordinal()); + } +} diff --git a/app/src/test/java/io/xeres/app/xrs/service/gxs/GxsSignatureTest.java b/app/src/test/java/io/xeres/app/xrs/service/gxs/GxsSignatureTest.java new file mode 100644 index 000000000..903d6cbb6 --- /dev/null +++ b/app/src/test/java/io/xeres/app/xrs/service/gxs/GxsSignatureTest.java @@ -0,0 +1,80 @@ +/* + * Copyright (c) 2019-2021 by David Gerber - https://zapek.com + * + * This file is part of Xeres. + * + * Xeres is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Xeres is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Xeres. If not, see . + */ + +package io.xeres.app.xrs.service.gxs; + +import io.netty.buffer.Unpooled; +import io.xeres.app.crypto.rsa.RSA; +import io.xeres.app.database.model.gxs.GxsIdGroupItemFakes; +import io.xeres.app.xrs.item.Item; +import io.xeres.app.xrs.item.RawItem; +import io.xeres.app.xrs.serialization.SerializationFlags; +import io.xeres.app.xrs.service.RsServiceType; +import org.junit.jupiter.api.Test; + +import java.security.NoSuchAlgorithmException; +import java.security.spec.InvalidKeySpecException; +import java.util.EnumSet; + +import static org.junit.jupiter.api.Assertions.assertNotNull; + +class GxsSignatureTest +{ + @Test + void GxsSignature_Create_And_Verify_OK() throws NoSuchAlgorithmException, InvalidKeySpecException + { + var gxsIdGroupItem = GxsIdGroupItemFakes.createGxsIdGroupItem(); + + var keyPair = RSA.generateKeys(512); + + gxsIdGroupItem.setAdminPrivateKeyData(keyPair.getPrivate().getEncoded()); + gxsIdGroupItem.setAdminPublicKeyData(keyPair.getPublic().getEncoded()); + + var data = serializeItemForSignature(gxsIdGroupItem); + + var signature = RSA.sign(data, RSA.getPrivateKey(gxsIdGroupItem.getAdminPrivateKeyData())); + gxsIdGroupItem.setSignature(signature); + + var rawItem = serializeItem(gxsIdGroupItem); + assertNotNull(rawItem); + +// var item = (GxsIdGroupItem) rawItem.deserialize(); // XXX: can't work like that... sigh. also GxsIdGroupItem is not in GxsId's item list :-/ +// assertNotNull(item); +// +// var verifyData = serializeItemForSignature(item); +// +// assertTrue(RSA.verify(RSA.getPublicKey(item.getAdminPublicKeyData()), item.getSignature(), verifyData)); + } + + private RawItem serializeItem(Item item) + { + item.setOutgoing(Unpooled.buffer().alloc(), 2, RsServiceType.GXSID, 1); + return item.serializeItem(EnumSet.noneOf(SerializationFlags.class)); + } + + private byte[] serializeItemForSignature(Item item) + { + item.setOutgoing(Unpooled.buffer().alloc(), 2, RsServiceType.GXSID, 1); + var buf = item.serializeItem(EnumSet.of(SerializationFlags.SIGNATURE)).getBuffer(); + var data = new byte[buf.writerIndex()]; + buf.getBytes(0, data); + buf.release(); + return data; + } +} diff --git a/app/src/test/java/io/xeres/app/xrs/service/gxs/SyncFlagsTest.java b/app/src/test/java/io/xeres/app/xrs/service/gxs/SyncFlagsTest.java new file mode 100644 index 000000000..53fc0b311 --- /dev/null +++ b/app/src/test/java/io/xeres/app/xrs/service/gxs/SyncFlagsTest.java @@ -0,0 +1,36 @@ +/* + * Copyright (c) 2019-2021 by David Gerber - https://zapek.com + * + * This file is part of Xeres. + * + * Xeres is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Xeres is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Xeres. If not, see . + */ + +package io.xeres.app.xrs.service.gxs; + +import org.junit.jupiter.api.Test; + +import static io.xeres.app.xrs.service.gxs.item.SyncFlags.REQUEST; +import static io.xeres.app.xrs.service.gxs.item.SyncFlags.RESPONSE; +import static org.junit.jupiter.api.Assertions.assertEquals; + +class SyncFlagsTest +{ + @Test + void SyncFlags_Enum_Order() + { + assertEquals(0, REQUEST.ordinal()); + assertEquals(1, RESPONSE.ordinal()); + } +} diff --git a/app/src/test/java/io/xeres/app/xrs/service/gxs/TransactionFlagsTest.java b/app/src/test/java/io/xeres/app/xrs/service/gxs/TransactionFlagsTest.java new file mode 100644 index 000000000..60bfb7119 --- /dev/null +++ b/app/src/test/java/io/xeres/app/xrs/service/gxs/TransactionFlagsTest.java @@ -0,0 +1,47 @@ +/* + * Copyright (c) 2019-2021 by David Gerber - https://zapek.com + * + * This file is part of Xeres. + * + * Xeres is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Xeres is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Xeres. If not, see . + */ + +package io.xeres.app.xrs.service.gxs; + +import org.junit.jupiter.api.Test; + +import static io.xeres.app.xrs.service.gxs.item.TransactionFlags.*; +import static org.junit.jupiter.api.Assertions.assertEquals; + +class TransactionFlagsTest +{ + @Test + void TransactionFlags_Enum_Order() + { + assertEquals(0, BEGIN_INCOMING.ordinal()); + assertEquals(1, BEGIN_OUTGOING.ordinal()); + assertEquals(2, END_SUCCESS.ordinal()); + assertEquals(3, CANCEL.ordinal()); + assertEquals(4, END_FAIL_NUM.ordinal()); + assertEquals(5, END_FAIL_TIMEOUT.ordinal()); + assertEquals(6, END_FAIL_FULL.ordinal()); + assertEquals(8, TYPE_GROUP_LIST_RESPONSE.ordinal()); + assertEquals(9, TYPE_MESSAGE_LIST_RESPONSE.ordinal()); + assertEquals(10, TYPE_GROUP_LIST_REQUEST.ordinal()); + assertEquals(11, TYPE_MESSAGE_LIST_REQUEST.ordinal()); + assertEquals(12, TYPE_GROUPS.ordinal()); + assertEquals(13, TYPE_MESSAGES.ordinal()); + assertEquals(14, TYPE_ENCRYPTED_DATA.ordinal()); + } +} diff --git a/app/src/test/java/io/xeres/app/xrs/service/heartbeat/HeartbeatTest.java b/app/src/test/java/io/xeres/app/xrs/service/heartbeat/HeartbeatTest.java new file mode 100644 index 000000000..1fa7989fe --- /dev/null +++ b/app/src/test/java/io/xeres/app/xrs/service/heartbeat/HeartbeatTest.java @@ -0,0 +1,45 @@ +/* + * Copyright (c) 2019-2021 by David Gerber - https://zapek.com + * + * This file is part of Xeres. + * + * Xeres is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Xeres is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Xeres. If not, see . + */ + +package io.xeres.app.xrs.service.heartbeat; + +import io.xeres.app.database.model.location.Location; +import io.xeres.app.net.peer.PeerConnection; +import io.xeres.app.xrs.service.heartbeat.item.HeartbeatItem; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.springframework.test.context.junit.jupiter.SpringExtension; + +@ExtendWith(SpringExtension.class) +class HeartbeatTest +{ + @InjectMocks + private HeartbeatService heartbeatService; + + @Test + void RttService_handleHeartbeat_OK() + { + var peerConnection = new PeerConnection(Location.createLocation("foo"), null); + + heartbeatService.handleItem(peerConnection, new HeartbeatItem()); + + // The service does nothing + } +} diff --git a/app/src/test/java/io/xeres/app/xrs/service/rtt/RttServiceTest.java b/app/src/test/java/io/xeres/app/xrs/service/rtt/RttServiceTest.java new file mode 100644 index 000000000..b5dbc7ea4 --- /dev/null +++ b/app/src/test/java/io/xeres/app/xrs/service/rtt/RttServiceTest.java @@ -0,0 +1,66 @@ +/* + * Copyright (c) 2019-2021 by David Gerber - https://zapek.com + * + * This file is part of Xeres. + * + * Xeres is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Xeres is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Xeres. If not, see . + */ + +package io.xeres.app.xrs.service.rtt; + +import io.xeres.app.database.model.location.Location; +import io.xeres.app.net.peer.PeerConnection; +import io.xeres.app.net.peer.PeerConnectionManager; +import io.xeres.app.xrs.service.RsService; +import io.xeres.app.xrs.service.rtt.item.RttPingItem; +import io.xeres.app.xrs.service.rtt.item.RttPongItem; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.ArgumentCaptor; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.springframework.test.context.junit.jupiter.SpringExtension; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotEquals; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.verify; + +@ExtendWith(SpringExtension.class) +class RttServiceTest +{ + @Mock + private PeerConnectionManager peerConnectionManager; + + @InjectMocks + private RttService rttService; + + @Test + void RttService_handlePing_OK() + { + int SEQUENCE = 1; + long TIMESTAMP = 2; + + var peerConnection = new PeerConnection(Location.createLocation("foo"), null); + + rttService.handleItem(peerConnection, new RttPingItem(SEQUENCE, TIMESTAMP)); + + ArgumentCaptor rttPongItem = ArgumentCaptor.forClass(RttPongItem.class); + verify(peerConnectionManager).writeItem(eq(peerConnection), rttPongItem.capture(), any(RsService.class)); + + assertEquals(TIMESTAMP, rttPongItem.getValue().getPingTimestamp()); + assertNotEquals(0, rttPongItem.getValue().getPongTimestamp()); + } +} diff --git a/app/src/test/java/io/xeres/app/xrs/service/status/StatusTest.java b/app/src/test/java/io/xeres/app/xrs/service/status/StatusTest.java new file mode 100644 index 000000000..fe5fd00b5 --- /dev/null +++ b/app/src/test/java/io/xeres/app/xrs/service/status/StatusTest.java @@ -0,0 +1,38 @@ +/* + * Copyright (c) 2019-2021 by David Gerber - https://zapek.com + * + * This file is part of Xeres. + * + * Xeres is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Xeres is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Xeres. If not, see . + */ + +package io.xeres.app.xrs.service.status; + +import org.junit.jupiter.api.Test; + +import static io.xeres.app.xrs.service.status.item.StatusItem.Status.*; +import static org.junit.jupiter.api.Assertions.assertEquals; + +class StatusTest +{ + @Test + void Status_Enum_Order() + { + assertEquals(0, OFFLINE.ordinal()); + assertEquals(1, AWAY.ordinal()); + assertEquals(2, BUSY.ordinal()); + assertEquals(3, ONLINE.ordinal()); + assertEquals(4, INACTIVE.ordinal()); + } +} diff --git a/app/src/test/java/io/xeres/testutils/FakeHTTPServer.java b/app/src/test/java/io/xeres/testutils/FakeHTTPServer.java new file mode 100644 index 000000000..c1152708c --- /dev/null +++ b/app/src/test/java/io/xeres/testutils/FakeHTTPServer.java @@ -0,0 +1,63 @@ +/* + * Copyright (c) 2019-2020 by David Gerber - https://zapek.com + * + * This file is part of Xeres. + * + * Xeres is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Xeres is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Xeres. If not, see . + */ + +package io.xeres.testutils; + +import com.sun.net.httpserver.HttpHandler; +import com.sun.net.httpserver.HttpServer; + +import java.io.IOException; +import java.net.InetSocketAddress; + +public class FakeHTTPServer +{ + public static final int LOCAL_PORT = 1068; // XXX: add a retry system when the port is taken + + private final HttpServer httpServer; + private byte[] requestBody; + + public FakeHTTPServer(String path, int responseCode, byte[] responseBody) throws IOException + { + var address = new InetSocketAddress(LOCAL_PORT); + httpServer = HttpServer.create(address, 0); + + HttpHandler handler = exchange -> { + requestBody = exchange.getRequestBody().readAllBytes(); + exchange.sendResponseHeaders(responseCode, responseBody != null ? responseBody.length : -1); + if (responseBody != null) + { + exchange.getResponseBody().write(responseBody); + } + exchange.close(); + }; + httpServer.createContext(path, handler); + + httpServer.start(); + } + + public byte[] getRequestBody() + { + return requestBody; + } + + public void shutdown() + { + httpServer.stop(0); + } +} diff --git a/app/src/test/java/io/xeres/testutils/TestUtils.java b/app/src/test/java/io/xeres/testutils/TestUtils.java new file mode 100644 index 000000000..3585fb927 --- /dev/null +++ b/app/src/test/java/io/xeres/testutils/TestUtils.java @@ -0,0 +1,45 @@ +/* + * Copyright (c) 2019-2020 by David Gerber - https://zapek.com + * + * This file is part of Xeres. + * + * Xeres is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Xeres is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Xeres. If not, see . + */ + +package io.xeres.testutils; + +import java.lang.reflect.Constructor; +import java.lang.reflect.InvocationTargetException; + +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.junit.jupiter.api.Assertions.assertFalse; + +public final class TestUtils +{ + private TestUtils() + { + throw new UnsupportedOperationException("Utility class"); + } + + public static void assertUtilityClass(Class javaClass) throws NoSuchMethodException + { + Constructor declaredConstructor = javaClass.getDeclaredConstructor(); + assertFalse(declaredConstructor.canAccess(null)); + declaredConstructor.setAccessible(true); + + assertThatThrownBy(declaredConstructor::newInstance) + .isInstanceOf(InvocationTargetException.class) + .hasCauseInstanceOf(UnsupportedOperationException.class); + } +} diff --git a/app/src/test/resources/application-default.properties b/app/src/test/resources/application-default.properties new file mode 100644 index 000000000..b6e700745 --- /dev/null +++ b/app/src/test/resources/application-default.properties @@ -0,0 +1,19 @@ +# +# Copyright (c) 2019-2021 by David Gerber - https://zapek.com +# +# This file is part of Xeres. +# +# Xeres is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Xeres is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Xeres. If not, see . +# +spring.datasource.url=jdbc:h2:mem:userdata \ No newline at end of file diff --git a/app/src/test/resources/upnp/routers/RT-AC87U.xml b/app/src/test/resources/upnp/routers/RT-AC87U.xml new file mode 100644 index 000000000..5389e9490 --- /dev/null +++ b/app/src/test/resources/upnp/routers/RT-AC87U.xml @@ -0,0 +1,77 @@ + + + + 1 + 1 + + + urn:schemas-upnp-org:device:InternetGatewayDevice:1 + RT-AC87U + ASUSTek + http://www.asus.com/ + ASUS Wireless Router + RT-AC87U + 384.13 + http://www.asus.com/ + 88:d7:f6:44:f8:d8 + uuid:3ddcd1d3-2380-45f5-b069-88d7f644f8d8 + + + urn:schemas-upnp-org:service:Layer3Forwarding:1 + urn:upnp-org:serviceId:L3Forwarding1 + /L3F.xml + /ctl/L3F + /evt/L3F + + + + + urn:schemas-upnp-org:device:WANDevice:1 + WANDevice + MiniUPnP + http://miniupnp.free.fr/ + WAN Device + WAN Device + 20200628 + http://miniupnp.free.fr/ + 88:d7:f6:44:f8:d8 + uuid:3ddcd1d3-2380-45f5-b069-88d7f644f8d9 + 000000000000 + + + urn:schemas-upnp-org:service:WANCommonInterfaceConfig:1 + urn:upnp-org:serviceId:WANCommonIFC1 + /WANCfg.xml + /ctl/CmnIfCfg + /evt/CmnIfCfg + + + + + urn:schemas-upnp-org:device:WANConnectionDevice:1 + WANConnectionDevice + MiniUPnP + http://miniupnp.free.fr/ + MiniUPnP daemon + MiniUPnPd + 20200628 + http://miniupnp.free.fr/ + 88:d7:f6:44:f8:d8 + uuid:3ddcd1d3-2380-45f5-b069-88d7f644f8da + 000000000000 + + + urn:schemas-upnp-org:service:WANIPConnection:1 + urn:upnp-org:serviceId:WANIPConn1 + /WANIPCn.xml + /ctl/IPConn + /evt/IPConn + + + + + + + http://192.168.1.1:80/ + + \ No newline at end of file diff --git a/build.gradle b/build.gradle new file mode 100644 index 000000000..2e4c8dc0e --- /dev/null +++ b/build.gradle @@ -0,0 +1,73 @@ +/* + * Copyright (c) 2019-2021 by David Gerber - https://zapek.com + * + * This file is part of Xeres. + * + * Xeres is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Xeres is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Xeres. If not, see . + */ + +buildscript { + ext { + flywayVersion = "7.14.1" + bouncycastleVersion = "1.69" + apacheCommonsLangVersion = "3.12.0" + apacheCommonsCollectionsVersion = "4.4" + springOpenApi = "1.5.10" + } +} + +plugins { + id 'org.springframework.boot' version '2.5.5' apply false + id 'org.flywaydb.flyway' version "$flywayVersion" apply false + id 'org.ec4j.editorconfig' version '0.0.3' apply false + id 'org.panteleyev.jpackageplugin' version '1.3.1' apply false +} + +// To upgrade Gradle, change the version here, refresh, then run the 'wrapper' task +wrapper { + gradleVersion = '7.2' +} + +subprojects { + group = 'io.xeres' + version = '0.1.2' + + apply plugin: 'io.spring.dependency-management' + apply plugin: 'java-library' + apply plugin: 'org.ec4j.editorconfig' + + java { + sourceCompatibility = JavaVersion.VERSION_17 + } + + repositories { + mavenCentral() + } + + dependencyManagement { + imports { + mavenBom org.springframework.boot.gradle.plugin.SpringBootPlugin.BOM_COORDINATES + } + } + + check.dependsOn editorconfigCheck +} + +task cleanProfile(type:Delete) { + group = 'build' + description = 'Deletes the user profile.' + // when run with IDEA Ultimate + delete 'data/userdata.mv.db' + delete 'data/userdata.trace.db' +} diff --git a/common/.gitignore b/common/.gitignore new file mode 100644 index 000000000..f3d6549d8 --- /dev/null +++ b/common/.gitignore @@ -0,0 +1 @@ +/build/ \ No newline at end of file diff --git a/common/build.gradle b/common/build.gradle new file mode 100644 index 000000000..cdebd2249 --- /dev/null +++ b/common/build.gradle @@ -0,0 +1,37 @@ +/* + * Copyright (c) 2019-2021 by David Gerber - https://zapek.com + * + * This file is part of Xeres. + * + * Xeres is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Xeres is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Xeres. If not, see . + */ + +editorconfig { + includes = ['src/**'] +} + +test { + useJUnitPlatform() + test.jvmArgs "-ea", "-Djava.net.preferIPv4Stack=true", "-Dfile.encoding=UTF-8" +} + +dependencies { + implementation 'org.springframework.boot:spring-boot-starter-validation' + implementation 'org.springframework.boot:spring-boot-starter-json' + implementation 'org.springframework.boot:spring-boot-starter-data-jpa' + implementation "org.apache.commons:commons-lang3:$apacheCommonsLangVersion" + implementation "org.springdoc:springdoc-openapi-ui:$springOpenApi" + testImplementation 'org.springframework.boot:spring-boot-starter-test' + +} diff --git a/common/src/main/java/io/xeres/common/AppName.java b/common/src/main/java/io/xeres/common/AppName.java new file mode 100644 index 000000000..fac7f56fb --- /dev/null +++ b/common/src/main/java/io/xeres/common/AppName.java @@ -0,0 +1,30 @@ +/* + * Copyright (c) 2019-2021 by David Gerber - https://zapek.com + * + * This file is part of Xeres. + * + * Xeres is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Xeres is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Xeres. If not, see . + */ + +package io.xeres.common; + +public final class AppName +{ + public static final String NAME = "Xeres"; + + private AppName() + { + throw new UnsupportedOperationException("Utility class"); + } +} diff --git a/common/src/main/java/io/xeres/common/dto/connection/ConnectionDTO.java b/common/src/main/java/io/xeres/common/dto/connection/ConnectionDTO.java new file mode 100644 index 000000000..acf8a6e13 --- /dev/null +++ b/common/src/main/java/io/xeres/common/dto/connection/ConnectionDTO.java @@ -0,0 +1,32 @@ +/* + * Copyright (c) 2019-2020 by David Gerber - https://zapek.com + * + * This file is part of Xeres. + * + * Xeres is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Xeres is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Xeres. If not, see . + */ + +package io.xeres.common.dto.connection; + +import java.time.Instant; + +// XXX: missing PeerAddress in the DTO +public record ConnectionDTO( + long id, + String address, + Instant lastConnected, + boolean external +) +{ +} diff --git a/common/src/main/java/io/xeres/common/dto/identity/IdentityConstants.java b/common/src/main/java/io/xeres/common/dto/identity/IdentityConstants.java new file mode 100644 index 000000000..3ff56aadb --- /dev/null +++ b/common/src/main/java/io/xeres/common/dto/identity/IdentityConstants.java @@ -0,0 +1,33 @@ +/* + * Copyright (c) 2019-2021 by David Gerber - https://zapek.com + * + * This file is part of Xeres. + * + * Xeres is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Xeres is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Xeres. If not, see . + */ + +package io.xeres.common.dto.identity; + +public final class IdentityConstants +{ + public static final int NAME_LENGTH_MIN = 1; + public static final int NAME_LENGTH_MAX = 64; + + public static final long OWN_IDENTITY_ID = 1L; // XXX: temporary + + private IdentityConstants() + { + throw new UnsupportedOperationException("Utility class"); + } +} diff --git a/common/src/main/java/io/xeres/common/dto/location/LocationConstants.java b/common/src/main/java/io/xeres/common/dto/location/LocationConstants.java new file mode 100644 index 000000000..f2c74dd66 --- /dev/null +++ b/common/src/main/java/io/xeres/common/dto/location/LocationConstants.java @@ -0,0 +1,33 @@ +/* + * Copyright (c) 2019-2020 by David Gerber - https://zapek.com + * + * This file is part of Xeres. + * + * Xeres is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Xeres is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Xeres. If not, see . + */ + +package io.xeres.common.dto.location; + +public final class LocationConstants +{ + public static final int NAME_LENGTH_MIN = 1; + public static final int NAME_LENGTH_MAX = 64; + + public static final long OWN_LOCATION_ID = 1L; + + private LocationConstants() + { + throw new UnsupportedOperationException("Utility class"); + } +} diff --git a/common/src/main/java/io/xeres/common/dto/location/LocationDTO.java b/common/src/main/java/io/xeres/common/dto/location/LocationDTO.java new file mode 100644 index 000000000..1f834de22 --- /dev/null +++ b/common/src/main/java/io/xeres/common/dto/location/LocationDTO.java @@ -0,0 +1,64 @@ +/* + * Copyright (c) 2019-2020 by David Gerber - https://zapek.com + * + * This file is part of Xeres. + * + * Xeres is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Xeres is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Xeres. If not, see . + */ + +package io.xeres.common.dto.location; + +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import io.xeres.common.dto.connection.ConnectionDTO; +import io.xeres.common.id.LocationId; + +import javax.validation.constraints.NotNull; +import javax.validation.constraints.Size; +import java.time.Instant; +import java.util.ArrayList; +import java.util.List; + +import static com.fasterxml.jackson.annotation.JsonInclude.Include.NON_EMPTY; + +public record LocationDTO( + + long id, + + @NotNull(message = "Name is mandatory") + @JsonProperty("name") + String name, + + @NotNull(message = "Location identifier is mandatory") + @Size(min = LocationId.LENGTH, max = LocationId.LENGTH) + byte[] locationIdentifier, + + String hostname, + + @JsonInclude(NON_EMPTY) + List connections, + + boolean connected, + + Instant lastConnected +) +{ + public LocationDTO + { + if (connections == null) + { + connections = new ArrayList<>(); + } + } +} diff --git a/common/src/main/java/io/xeres/common/dto/profile/ProfileConstants.java b/common/src/main/java/io/xeres/common/dto/profile/ProfileConstants.java new file mode 100644 index 000000000..e8306fb77 --- /dev/null +++ b/common/src/main/java/io/xeres/common/dto/profile/ProfileConstants.java @@ -0,0 +1,33 @@ +/* + * Copyright (c) 2019-2020 by David Gerber - https://zapek.com + * + * This file is part of Xeres. + * + * Xeres is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Xeres is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Xeres. If not, see . + */ + +package io.xeres.common.dto.profile; + +public final class ProfileConstants +{ + public static final int NAME_LENGTH_MIN = 1; + public static final int NAME_LENGTH_MAX = 64; + + public static final long OWN_PROFILE_ID = 1L; + + private ProfileConstants() + { + throw new UnsupportedOperationException("Utility class"); + } +} diff --git a/common/src/main/java/io/xeres/common/dto/profile/ProfileDTO.java b/common/src/main/java/io/xeres/common/dto/profile/ProfileDTO.java new file mode 100644 index 000000000..fb17fd24e --- /dev/null +++ b/common/src/main/java/io/xeres/common/dto/profile/ProfileDTO.java @@ -0,0 +1,68 @@ +/* + * Copyright (c) 2019-2020 by David Gerber - https://zapek.com + * + * This file is part of Xeres. + * + * Xeres is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Xeres is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Xeres. If not, see . + */ + +package io.xeres.common.dto.profile; + +import com.fasterxml.jackson.annotation.JsonInclude; +import io.swagger.v3.oas.annotations.media.Schema; +import io.xeres.common.dto.location.LocationDTO; +import io.xeres.common.id.ProfileFingerprint; +import io.xeres.common.pgp.Trust; + +import javax.validation.constraints.NotNull; +import javax.validation.constraints.Size; +import java.util.ArrayList; +import java.util.List; + +import static com.fasterxml.jackson.annotation.JsonInclude.Include.NON_EMPTY; +import static io.xeres.common.dto.profile.ProfileConstants.NAME_LENGTH_MAX; +import static io.xeres.common.dto.profile.ProfileConstants.NAME_LENGTH_MIN; + +public record ProfileDTO( + + long id, + + @NotNull(message = "Name is mandatory") + @Size(message = "Name length must be between " + NAME_LENGTH_MIN + " and " + NAME_LENGTH_MAX + " characters", min = NAME_LENGTH_MIN, max = NAME_LENGTH_MAX) + String name, + + String pgpIdentifier, + + @Size(min = ProfileFingerprint.LENGTH, max = ProfileFingerprint.LENGTH) + @Schema(example = "nhgF6ITwm/LLqchhpwJ91KFfAxg=") + byte[] pgpFingerprint, + + byte[] pgpPublicKeyData, + + boolean accepted, + + Trust trust, + + @JsonInclude(NON_EMPTY) + List locations +) +{ + public ProfileDTO + { + if (locations == null) + { + locations = new ArrayList<>(); + } + } +} diff --git a/common/src/main/java/io/xeres/common/id/GxsId.java b/common/src/main/java/io/xeres/common/id/GxsId.java new file mode 100644 index 000000000..40982ce8f --- /dev/null +++ b/common/src/main/java/io/xeres/common/id/GxsId.java @@ -0,0 +1,82 @@ +/* + * Copyright (c) 2019-2021 by David Gerber - https://zapek.com + * + * This file is part of Xeres. + * + * Xeres is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Xeres is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Xeres. If not, see . + */ + +package io.xeres.common.id; + +import javax.persistence.Embeddable; +import java.util.Arrays; + +@Embeddable +public class GxsId implements Identifier +{ + public static final int LENGTH = 16; + + private byte[] identifier; + + public GxsId() + { + + } + + public GxsId(byte[] identifier) + { + if (identifier == null) + { + throw new IllegalArgumentException("Null identifier"); + } + if (identifier.length != LENGTH) + { + throw new IllegalArgumentException("Bad identifier length, expected " + LENGTH + ", got " + identifier.length); + } + this.identifier = identifier; + } + + @Override + public byte[] getBytes() + { + return identifier; + } + + @Override + public int getLength() + { + return LENGTH; + } + + @Override + public boolean equals(Object o) + { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + var gxsId = (GxsId) o; + return Arrays.equals(identifier, gxsId.identifier); + } + + @Override + public int hashCode() + { + return Arrays.hashCode(identifier); + } + + @Override + public String toString() + { + return Id.toString(identifier); + } +} diff --git a/common/src/main/java/io/xeres/common/id/Id.java b/common/src/main/java/io/xeres/common/id/Id.java new file mode 100644 index 000000000..6575e2e4b --- /dev/null +++ b/common/src/main/java/io/xeres/common/id/Id.java @@ -0,0 +1,204 @@ +/* + * Copyright (c) 2019-2021 by David Gerber - https://zapek.com + * + * This file is part of Xeres. + * + * Xeres is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Xeres is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Xeres. If not, see . + */ + +package io.xeres.common.id; + +import org.apache.commons.lang3.ArrayUtils; + +import java.util.Locale; + +import static org.apache.commons.lang3.StringUtils.isEmpty; + +public final class Id +{ + private static final char[] hex = "0123456789abcdef".toCharArray(); + + private Id() + { + throw new UnsupportedOperationException("Utility class"); + } + + /** + * Converts a series of bytes into its hexadecimal representation. For example if the + * byte array contains 2 bytes like 28 then 3, the result is "1c03". + * + * @param id the id as a stream of bytes + * @return the lowercase hexadecimal representation of those bytes, without any prefix or an empty string if the id is null or empty + */ + public static String toString(byte[] id) + { + if (ArrayUtils.isEmpty(id)) + { + return ""; + } + + var sb = new StringBuilder(id.length * 2); + + for (byte b : id) + { + sb.append(hex[(b & 0xf0) >> 4]) + .append(hex[b & 0x0f]); + } + return sb.toString(); + } + + /** + * Converts an hexadecimal string into an array of bytes. For example + * if the input contains "1c03", then the result is an array of 2 bytes with 28 then 3. + * + * @param id the values as a lowercase hexadecimal series of bytes, without prefix + * @return an array of bytes containing those values or an empty array if the id is null or empty + */ + public static byte[] toBytes(String id) + { + if (isEmpty(id)) + { + return new byte[0]; + } + + var out = new byte[id.length() / 2]; + + for (var i = 0; i < out.length; i++) + { + int index = i * 2; + out[i] = (byte) Integer.parseUnsignedInt(id.substring(index, index + 2), 16); + } + return out; + } + + /** + * Converts an id into its hexadecimal representation. + * + * @param id the id + * @return an hexadecimal uppercase representation of the id, without prefix + */ + public static String toString(long id) + { + return Long.toHexString(id).toUpperCase(Locale.ROOT); + } + + /** + * Converts an id into its hexadecimal representation. + * + * @param id the id + * @return an hexadecimal lowercase representation of the id, without prefix + */ + public static String toStringLowerCase(long id) + { + return Long.toHexString(id); + } + + /** + * Converts an identifier into its hexadecimal representation. + * + * @param identifier the identifier + * @return an hexadecimal lowercase representation of the identifier, without prefix + */ + public static String toString(Identifier identifier) + { + return toString(identifier.getBytes()); + } + + public static byte[] asciiStringToBytes(String id) + { + return asciiToBytes(id.getBytes()); + } + + /** + * Converts an array containing an hexadecimal ASCII representation of bytes into an array of + * the corresponding byte values. For example, if the array contains 0x31 ('1') and 0x33 ('3') + * which represents 0x13, the result is an array of bytes which is { 0x13 }. + * + * @param id an array of hexadecimal ASCII values + * @return an array of corresponding values + */ + public static byte[] asciiToBytes(byte[] id) + { + if (ArrayUtils.isEmpty(id)) + { + throw new IllegalArgumentException("id is null or empty"); + } + + if (id.length % 2 == 1) + { + throw new IllegalArgumentException("id is not even"); + } + + var result = new byte[id.length / 2]; + byte number; + var accumulator = 0; + + for (var i = 0; i < id.length; i++) + { + number = id[i]; + + if (number >= 'a') + { + if (number > 'f') + { + throw new IllegalArgumentException("id has an invalid ascii value: " + number); + } + number -= 'a'; + number += 10; + } + else if (number >= '0') + { + number -= '0'; + } + else + { + throw new IllegalArgumentException("id has an invalid ascii value: " + number); + } + + if (i % 2 == 1) + { + result[i / 2] = (byte) (accumulator * 16 + number); + } + else + { + accumulator = number; + } + } + return result; + } + + /** + * Converts an identifier to its ASCII representation. For example if the identifier is 0x12, then its + * ASCII representation will be { 0x31, 0x32 } ('1' and '2'). + * + * @param identifier an identifier + * @return the byte array containing the ASCII values of each number of the identifier. The array is twice as long as the input + */ + public static byte[] toAsciiBytes(Identifier identifier) + { + return Id.toString(identifier).getBytes(); + } + + /** + * Same as {@link #toAsciiBytes(Identifier)} but in upper case. + * + * @param identifier an identifier + * @return the byte array containing the ASCII values of each number of the identifier in upper case. The array + * is twice as long as the input + */ + public static byte[] toAsciiBytesUpperCase(Identifier identifier) + { + return Id.toString(identifier).toUpperCase(Locale.ROOT).getBytes(); + } +} diff --git a/common/src/main/java/io/xeres/common/id/Identifier.java b/common/src/main/java/io/xeres/common/id/Identifier.java new file mode 100644 index 000000000..df023bdb4 --- /dev/null +++ b/common/src/main/java/io/xeres/common/id/Identifier.java @@ -0,0 +1,58 @@ +/* + * Copyright (c) 2019-2020 by David Gerber - https://zapek.com + * + * This file is part of Xeres. + * + * Xeres is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Xeres is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Xeres. If not, see . + */ + +package io.xeres.common.id; + +import java.util.Arrays; + +/** + * Interface that represents an identifier of an object in the Retroshare protocol that doesn't fit + * in a primitive type. + */ +public interface Identifier +{ + /** + * Gets a byte representation of the identifier. + * + * @return an array of bytes containing the identifier + */ + byte[] getBytes(); + + /** + * Gets how many bytes are needed to store the identifier. + * + * @return the length of the identifier + */ + int getLength(); + + /** + * Gets the representation of the identifier. To be used every time the identity is needed + * as a string (UI, headers, etc...). + * + * @return a string representation + */ + String toString(); + + default byte[] getNullIdentifier() + { + var identifier = new byte[getLength()]; + Arrays.fill(identifier, (byte) 0); + return identifier; + } +} diff --git a/common/src/main/java/io/xeres/common/id/LocationId.java b/common/src/main/java/io/xeres/common/id/LocationId.java new file mode 100644 index 000000000..5c7e08463 --- /dev/null +++ b/common/src/main/java/io/xeres/common/id/LocationId.java @@ -0,0 +1,97 @@ +/* + * Copyright (c) 2019-2021 by David Gerber - https://zapek.com + * + * This file is part of Xeres. + * + * Xeres is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Xeres is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Xeres. If not, see . + */ + +package io.xeres.common.id; + +import javax.persistence.Embeddable; +import java.util.Arrays; + +import static org.apache.commons.lang3.StringUtils.isEmpty; + +@Embeddable +public class LocationId implements Identifier +{ + public static final int LENGTH = 16; + + private byte[] identifier; + + public LocationId() + { + + } + + public LocationId(byte[] identifier) + { + if (identifier == null) + { + throw new IllegalArgumentException("Null identifier"); + } + if (identifier.length != LENGTH) + { + throw new IllegalArgumentException("Bad identifier length, expected " + LENGTH + ", got " + identifier.length); + } + this.identifier = identifier; + } + + public LocationId(String identifier) + { + if (isEmpty(identifier)) + { + throw new IllegalArgumentException("Empty identifier"); + } + if (identifier.length() != LENGTH * 2) + { + throw new IllegalArgumentException("Bad identifier length, expected " + (LENGTH * 2) + ", got " + identifier.length()); + } + this.identifier = Id.toBytes(identifier); + } + + @Override + public byte[] getBytes() + { + return identifier; + } + + @Override + public int getLength() + { + return LENGTH; + } + + @Override + public boolean equals(Object o) + { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + LocationId that = (LocationId) o; + return Arrays.equals(identifier, that.identifier); + } + + @Override + public int hashCode() + { + return Arrays.hashCode(identifier); + } + + @Override + public String toString() + { + return Id.toString(identifier); + } +} diff --git a/common/src/main/java/io/xeres/common/id/ProfileFingerprint.java b/common/src/main/java/io/xeres/common/id/ProfileFingerprint.java new file mode 100644 index 000000000..ab954568e --- /dev/null +++ b/common/src/main/java/io/xeres/common/id/ProfileFingerprint.java @@ -0,0 +1,93 @@ +/* + * Copyright (c) 2019-2021 by David Gerber - https://zapek.com + * + * This file is part of Xeres. + * + * Xeres is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Xeres is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Xeres. If not, see . + */ + +package io.xeres.common.id; + +import javax.persistence.Embeddable; +import java.util.Arrays; + +@Embeddable +public class ProfileFingerprint implements Identifier +{ + public static final int LENGTH = 20; + + private byte[] identifier; + + public ProfileFingerprint() + { + + } + + public ProfileFingerprint(byte[] identifier) + { + if (identifier == null) + { + throw new IllegalArgumentException("Null identifier"); + } + if (identifier.length != LENGTH) + { + throw new IllegalArgumentException("Bad identifier length, expected " + LENGTH + ", got " + identifier.length); + } + this.identifier = identifier; + } + + @Override + public byte[] getBytes() + { + return identifier; + } + + @Override + public int getLength() + { + return LENGTH; + } + + @Override + public boolean equals(Object o) + { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + ProfileFingerprint that = (ProfileFingerprint) o; + return Arrays.equals(identifier, that.identifier); + } + + @Override + public int hashCode() + { + return Arrays.hashCode(identifier); + } + + @Override + public String toString() + { + var s = Id.toString(identifier); + var out = new StringBuilder(); + + for (var i = 0; i < 40; i += 4) + { + if (i > 0) + { + out.append(" "); + } + out.append(s, i, i + 4); + } + return out.toString(); + } +} diff --git a/common/src/main/java/io/xeres/common/id/Sha1Sum.java b/common/src/main/java/io/xeres/common/id/Sha1Sum.java new file mode 100644 index 000000000..b63fa03ee --- /dev/null +++ b/common/src/main/java/io/xeres/common/id/Sha1Sum.java @@ -0,0 +1,82 @@ +/* + * Copyright (c) 2019-2021 by David Gerber - https://zapek.com + * + * This file is part of Xeres. + * + * Xeres is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Xeres is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Xeres. If not, see . + */ + +package io.xeres.common.id; + +import javax.persistence.Embeddable; +import java.util.Arrays; + +@Embeddable +public class Sha1Sum implements Identifier +{ + public static final int LENGTH = 20; + + private byte[] identifier; + + public Sha1Sum() + { + + } + + public Sha1Sum(byte[] sum) + { + if (sum == null) + { + throw new IllegalArgumentException("Null sha1sum"); + } + if (sum.length != LENGTH) + { + throw new IllegalArgumentException("Bad sha1sum length, expected " + LENGTH + ", got " + sum.length); + } + this.identifier = sum; + } + + @Override + public byte[] getBytes() + { + return identifier; + } + + @Override + public int getLength() + { + return LENGTH; + } + + @Override + public boolean equals(Object o) + { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + Sha1Sum that = (Sha1Sum) o; + return Arrays.equals(identifier, that.identifier); + } + + @Override + public int hashCode() + { + return Arrays.hashCode(identifier); + } + + @Override + public String toString() + { + return Id.toString(identifier); + } +} diff --git a/common/src/main/java/io/xeres/common/identity/Type.java b/common/src/main/java/io/xeres/common/identity/Type.java new file mode 100644 index 000000000..c637821ec --- /dev/null +++ b/common/src/main/java/io/xeres/common/identity/Type.java @@ -0,0 +1,40 @@ +/* + * Copyright (c) 2019-2021 by David Gerber - https://zapek.com + * + * This file is part of Xeres. + * + * Xeres is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Xeres is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Xeres. If not, see . + */ + +package io.xeres.common.identity; + +public enum Type +{ + /** + * Own identity signed by own profile. + */ + SIGNED, + /** + * Own identity, unsigned. + */ + ANONYMOUS, + /** + * Identity owned by a friend. + */ + FRIEND, + /** + * Other identity. Not saved to the identities table. + */ + OTHER +} diff --git a/common/src/main/java/io/xeres/common/message/MessageHeaders.java b/common/src/main/java/io/xeres/common/message/MessageHeaders.java new file mode 100644 index 000000000..ddf65c7cd --- /dev/null +++ b/common/src/main/java/io/xeres/common/message/MessageHeaders.java @@ -0,0 +1,50 @@ +/* + * Copyright (c) 2019-2021 by David Gerber - https://zapek.com + * + * This file is part of Xeres. + * + * Xeres is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Xeres is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Xeres. If not, see . + */ + +package io.xeres.common.message; + +import java.util.HashMap; +import java.util.Map; + +public final class MessageHeaders +{ + public static final String MESSAGE_TYPE = "messageType"; + public static final String DESTINATION_ID = "destinationId"; + + private MessageHeaders() + { + throw new UnsupportedOperationException("Utility class"); + } + + public static Map buildMessageHeaders(MessageType messageType, String id) + { + Map headers = new HashMap<>(); + headers.put(MESSAGE_TYPE, messageType.name()); + if (id != null) + { + headers.put(DESTINATION_ID, id); + } + return headers; + } + + public static Map buildMessageHeaders(MessageType messageType) + { + return buildMessageHeaders(messageType, null); + } +} diff --git a/common/src/main/java/io/xeres/common/message/MessageType.java b/common/src/main/java/io/xeres/common/message/MessageType.java new file mode 100644 index 000000000..89e67e62c --- /dev/null +++ b/common/src/main/java/io/xeres/common/message/MessageType.java @@ -0,0 +1,32 @@ +/* + * Copyright (c) 2019-2021 by David Gerber - https://zapek.com + * + * This file is part of Xeres. + * + * Xeres is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Xeres is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Xeres. If not, see . + */ + +package io.xeres.common.message; + +public enum MessageType +{ + CHAT_PRIVATE_MESSAGE, + CHAT_ROOM_MESSAGE, + CHAT_ROOM_LIST, + CHAT_BROADCAST_MESSAGE, + CHAT_TYPING_NOTIFICATION, + CHAT_ROOM_JOIN, + CHAT_ROOM_LEAVE, + CHAT_ROOM_TYPING_NOTIFICATION +} diff --git a/common/src/main/java/io/xeres/common/message/chat/ChatConstants.java b/common/src/main/java/io/xeres/common/message/chat/ChatConstants.java new file mode 100644 index 000000000..15da491c0 --- /dev/null +++ b/common/src/main/java/io/xeres/common/message/chat/ChatConstants.java @@ -0,0 +1,32 @@ +/* + * Copyright (c) 2019-2021 by David Gerber - https://zapek.com + * + * This file is part of Xeres. + * + * Xeres is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Xeres is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Xeres. If not, see . + */ + +package io.xeres.common.message.chat; + +import java.time.Duration; + +public final class ChatConstants +{ + public static final Duration TYPING_NOTIFICATION_DELAY = Duration.ofSeconds(5); + + private ChatConstants() + { + throw new UnsupportedOperationException("Utility class"); + } +} diff --git a/common/src/main/java/io/xeres/common/message/chat/ChatMessage.java b/common/src/main/java/io/xeres/common/message/chat/ChatMessage.java new file mode 100644 index 000000000..8cb615920 --- /dev/null +++ b/common/src/main/java/io/xeres/common/message/chat/ChatMessage.java @@ -0,0 +1,62 @@ +/* + * Copyright (c) 2019-2021 by David Gerber - https://zapek.com + * + * This file is part of Xeres. + * + * Xeres is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Xeres is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Xeres. If not, see . + */ + +package io.xeres.common.message.chat; + +/** + * Used to send messages from a chat client to a web socket only. + * If a chat message has no content, it's a notification. + */ +public class ChatMessage +{ + private String content; + + public ChatMessage() + { + // Needed for JSON + } + + public ChatMessage(String message) + { + this.content = message; + } + + public String getContent() + { + return content; + } + + public void setContent(String content) + { + this.content = content; + } + + public boolean isEmpty() + { + return content == null; + } + + @Override + public String toString() + { + return "ChatMessage{" + + "content='" + content + "'" + + '}'; + } +} diff --git a/common/src/main/java/io/xeres/common/message/chat/ChatRoomListMessage.java b/common/src/main/java/io/xeres/common/message/chat/ChatRoomListMessage.java new file mode 100644 index 000000000..df3c5937d --- /dev/null +++ b/common/src/main/java/io/xeres/common/message/chat/ChatRoomListMessage.java @@ -0,0 +1,38 @@ +/* + * Copyright (c) 2019-2021 by David Gerber - https://zapek.com + * + * This file is part of Xeres. + * + * Xeres is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Xeres is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Xeres. If not, see . + */ + +package io.xeres.common.message.chat; + +import java.util.ArrayList; +import java.util.List; + +public class ChatRoomListMessage +{ + private final List rooms = new ArrayList<>(); + + public void add(RoomInfo roomInfo) + { + rooms.add(roomInfo); + } + + public List getRooms() + { + return rooms; + } +} diff --git a/common/src/main/java/io/xeres/common/message/chat/ChatRoomMessage.java b/common/src/main/java/io/xeres/common/message/chat/ChatRoomMessage.java new file mode 100644 index 000000000..a0d614673 --- /dev/null +++ b/common/src/main/java/io/xeres/common/message/chat/ChatRoomMessage.java @@ -0,0 +1,83 @@ +/* + * Copyright (c) 2019-2021 by David Gerber - https://zapek.com + * + * This file is part of Xeres. + * + * Xeres is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Xeres is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Xeres. If not, see . + */ + +package io.xeres.common.message.chat; + +public class ChatRoomMessage +{ + private long roomId; + private String senderNickname; + private String content; + + public ChatRoomMessage() + { + // Needed for JSON + } + + public ChatRoomMessage(String senderNickname, String content) + { + this.senderNickname = senderNickname; + this.content = content; + } + + public long getRoomId() + { + return roomId; + } + + public void setRoomId(long roomId) + { + this.roomId = roomId; + } + + public String getSenderNickname() + { + return senderNickname; + } + + public void setSenderNickname(String senderNickname) + { + this.senderNickname = senderNickname; + } + + public String getContent() + { + return content; + } + + public void setContent(String content) + { + this.content = content; + } + + public boolean isEmpty() + { + return content == null; + } + + @Override + public String toString() + { + return "ChatRoomMessage{" + + "roomId=" + roomId + + ", senderNickname='" + senderNickname + '\'' + + ", content='" + content + '\'' + + '}'; + } +} diff --git a/common/src/main/java/io/xeres/common/message/chat/PrivateChatMessage.java b/common/src/main/java/io/xeres/common/message/chat/PrivateChatMessage.java new file mode 100644 index 000000000..ff729ea21 --- /dev/null +++ b/common/src/main/java/io/xeres/common/message/chat/PrivateChatMessage.java @@ -0,0 +1,58 @@ +/* + * Copyright (c) 2019-2021 by David Gerber - https://zapek.com + * + * This file is part of Xeres. + * + * Xeres is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Xeres is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Xeres. If not, see . + */ + +package io.xeres.common.message.chat; + +public class PrivateChatMessage +{ + private String content; + + public PrivateChatMessage() + { + // Needed for JSON + } + + public PrivateChatMessage(String message) + { + this.content = message; + } + + public String getContent() + { + return content; + } + + public void setContent(String content) + { + this.content = content; + } + + public boolean isEmpty() + { + return content == null; + } + + @Override + public String toString() + { + return "ChatMessage{" + + "content='" + content + "'" + + '}'; + } +} diff --git a/common/src/main/java/io/xeres/common/message/chat/RoomInfo.java b/common/src/main/java/io/xeres/common/message/chat/RoomInfo.java new file mode 100644 index 000000000..027892e6e --- /dev/null +++ b/common/src/main/java/io/xeres/common/message/chat/RoomInfo.java @@ -0,0 +1,133 @@ +/* + * Copyright (c) 2019-2021 by David Gerber - https://zapek.com + * + * This file is part of Xeres. + * + * Xeres is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Xeres is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Xeres. If not, see . + */ + +package io.xeres.common.message.chat; + +import java.util.Objects; + +public class RoomInfo +{ + private long id; + private String name; + private RoomType roomType; + private String topic; + private int count; + private boolean isSigned; + + public RoomInfo() + { + + } + + public RoomInfo(String name) + { + this.name = name; + } + + public RoomInfo(long id, String name, RoomType roomType, String topic, int count, boolean isSigned) + { + this.id = id; + this.name = name; + this.roomType = roomType; + this.topic = topic; + this.count = count; + this.isSigned = isSigned; + } + + public long getId() + { + return id; + } + + public void setId(long id) + { + this.id = id; + } + + public String getName() + { + return name; + } + + public void setName(String name) + { + this.name = name; + } + + public RoomType getRoomType() + { + return roomType; + } + + public void setRoomType(RoomType roomType) + { + this.roomType = roomType; + } + + public String getTopic() + { + return topic; + } + + public void setTopic(String topic) + { + this.topic = topic; + } + + public int getCount() + { + return count; + } + + public void setCount(int count) + { + this.count = count; + } + + public boolean isSigned() + { + return isSigned; + } + + public void setSigned(boolean signed) + { + isSigned = signed; + } + + @Override + public boolean equals(Object o) + { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + var roomInfo = (RoomInfo) o; + return id == roomInfo.id; + } + + @Override + public int hashCode() + { + return Objects.hash(id); + } + + @Override + public String toString() + { + return name; + } +} diff --git a/common/src/main/java/io/xeres/common/message/chat/RoomType.java b/common/src/main/java/io/xeres/common/message/chat/RoomType.java new file mode 100644 index 000000000..ada1017f5 --- /dev/null +++ b/common/src/main/java/io/xeres/common/message/chat/RoomType.java @@ -0,0 +1,34 @@ +/* + * Copyright (c) 2019-2021 by David Gerber - https://zapek.com + * + * This file is part of Xeres. + * + * Xeres is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Xeres is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Xeres. If not, see . + */ + +package io.xeres.common.message.chat; + +import java.util.Locale; + +public enum RoomType +{ + PRIVATE, + PUBLIC; + + @Override + public String toString() + { + return super.toString().toLowerCase(Locale.ROOT); + } +} diff --git a/common/src/main/java/io/xeres/common/pgp/Trust.java b/common/src/main/java/io/xeres/common/pgp/Trust.java new file mode 100644 index 000000000..0e83259f6 --- /dev/null +++ b/common/src/main/java/io/xeres/common/pgp/Trust.java @@ -0,0 +1,66 @@ +/* + * Copyright (c) 2019-2020 by David Gerber - https://zapek.com + * + * This file is part of Xeres. + * + * Xeres is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Xeres is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Xeres. If not, see . + */ + +package io.xeres.common.pgp; + +import java.util.Locale; + +/** + * This is the trust level for a PGP-like "web of trust" feature. Note that + * 'undefined' is not here because it's confusing. + *

+ * Note: this is stored in the database in ordinal. Do not modify the order. + */ +public enum Trust +{ + /** + * No opinion about the trustworthiness of the owner. + */ + UNKNOWN, + + /** + * No trust about the owner. Ie. he's known to sign stuff without + * checking or without the other owner's consent. + */ + NEVER, + + /** + * Trust that the owner doesn't perform certifications blindly but not + * very accurately either. Trust will only become valid after multiple certifications (usually 3). + * A good default choice. + */ + MARGINAL, + + /** + * Trust that the owner performs certification very accurately. Trust + * will become valid after a single one so use with care. + */ + FULL, + + /** + * Our own key. + */ + ULTIMATE; + + @Override + public String toString() + { + return super.toString().toLowerCase(Locale.ENGLISH); + } +} diff --git a/common/src/main/java/io/xeres/common/properties/StartupProperties.java b/common/src/main/java/io/xeres/common/properties/StartupProperties.java new file mode 100644 index 000000000..0ab224668 --- /dev/null +++ b/common/src/main/java/io/xeres/common/properties/StartupProperties.java @@ -0,0 +1,174 @@ +/* + * Copyright (c) 2019-2021 by David Gerber - https://zapek.com + * + * This file is part of Xeres. + * + * Xeres is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Xeres is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Xeres. If not, see . + */ + +package io.xeres.common.properties; + +import io.xeres.common.protocol.ip.IP; +import org.apache.commons.lang3.StringUtils; + +public final class StartupProperties +{ + public enum Property + { + SERVER_ONLY("xrs.network.server-only", Boolean.class), + CONTROL_PORT("server.port", Integer.class), + SERVER_PORT("xrs.network.server-port", Integer.class), + DATA_DIR("xrs.data.dir-path", String.class), + UI("xrs.ui.enabled", Boolean.class), + UI_ADDRESS("xrs.ui.address", String.class), + UI_PORT("xrs.ui.port", Integer.class), + FAST_SHUTDOWN("xrs.network.fast-shutdown", Boolean.class); + + Property(String property, Class javaClass) + { + this.property = property; + this.javaClass = javaClass; + } + + private final String property; + private final Class javaClass; + + public String getKey() + { + return property; + } + + public Class getJavaClass() + { + return javaClass; + } + } + + private StartupProperties() + { + throw new UnsupportedOperationException("Utility class"); + } + + public static String getString(Property property, String defaultValue) + { + return System.getProperty(property.getKey(), defaultValue); + } + + public static String getString(Property property) + { + return System.getProperty(property.getKey()); + } + + public static void setString(Property property, String value) + { + assert property.getJavaClass().equals(String.class); + + if (StringUtils.isBlank(value)) + { + throw new IllegalArgumentException("Property " + property.name() + " (" + property.getKey() + ") does not contain a value"); + } + + System.setProperty(property.getKey(), value); + } + + public static Boolean getBoolean(Property property) + { + String value = System.getProperty(property.getKey()); + if (value == null) + { + return null; + } + return Boolean.parseBoolean(value); + } + + @SuppressWarnings("BooleanParameter") + public static boolean getBoolean(Property property, boolean defaultValue) + { + String value = System.getProperty(property.getKey()); + if (value == null) + { + return defaultValue; + } + return Boolean.parseBoolean(value); + } + + public static void setBoolean(Property property, String value) + { + assert property.getJavaClass().equals(Boolean.class); + + boolean val = value.equals("1") || value.equalsIgnoreCase("yes") || Boolean.parseBoolean(value); + if (!val) + { + if (!(value.equals("0") || value.equalsIgnoreCase("no") || value.equalsIgnoreCase("false"))) + { + throw new IllegalArgumentException("Property " + property.name() + " (" + property.getKey() + ") does not contain a boolean value (" + value + ")"); + } + } + System.setProperty(property.getKey(), String.valueOf(val)); + } + + public static Integer getInteger(Property property) + { + String value = System.getProperty(property.getKey()); + if (value == null) + { + return null; + } + return Integer.parseInt(value); + } + + public static void setPort(Property property, String value) + { + assert property.getJavaClass().equals(Integer.class); + + try + { + var val = Integer.parseUnsignedInt(value); + if (!IP.isValidPort(val)) + { + throw new NumberFormatException(); + } + System.setProperty(property.getKey(), String.valueOf(val)); + } + catch (NumberFormatException e) + { + throw new IllegalArgumentException("Property " + property.name() + " (" + property.getKey() + ") does not contain a port bigger than 0 and smaller than 65536 (" + value + ")"); + } + } + + public static void setUiRemoteConnect(String ipAndPort) + { + String[] tokens = ipAndPort.split(":"); + + if (StringUtils.isBlank(tokens[0])) + { + throw new IllegalArgumentException("Missing hostname"); + } + if (!IP.isBindableIp(tokens[0])) + { + throw new IllegalArgumentException("IP " + tokens[0] + " cannot be bound to"); + } + setString(Property.UI_ADDRESS, tokens[0]); + + if (tokens.length == 2 && StringUtils.isNotBlank(tokens[1])) + { + if (!IP.isValidPort(Integer.parseUnsignedInt(tokens[1]))) + { + throw new IllegalArgumentException("Invalid port " + tokens[1]); + } + setPort(Property.UI_PORT, tokens[1]); + } + System.setProperty("spring.main.web-application-type", "none"); + } +} diff --git a/common/src/main/java/io/xeres/common/protocol/NetMode.java b/common/src/main/java/io/xeres/common/protocol/NetMode.java new file mode 100644 index 000000000..ae9edb797 --- /dev/null +++ b/common/src/main/java/io/xeres/common/protocol/NetMode.java @@ -0,0 +1,42 @@ +/* + * Copyright (c) 2019-2021 by David Gerber - https://zapek.com + * + * This file is part of Xeres. + * + * Xeres is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Xeres is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Xeres. If not, see . + */ + +package io.xeres.common.protocol; + +import java.util.Locale; + +/** + * The NetMode
+ * Note: this is stored in the database in ordinal. Do not modify the order. + */ +public enum NetMode +{ + UNKNOWN, // Unknown netmode + UDP, // firewalled | UDP mode + UPNP, // automatic (UPNP) | Ext (UPNP) + EXT, // manually forwarded port | External port + HIDDEN, // hidden mode | Hidden + UNREACHABLE; // UDP mode (unreachable) + + @Override + public String toString() + { + return super.toString().toLowerCase(Locale.ENGLISH); + } +} diff --git a/common/src/main/java/io/xeres/common/protocol/ip/IP.java b/common/src/main/java/io/xeres/common/protocol/ip/IP.java new file mode 100644 index 000000000..960ae8e0b --- /dev/null +++ b/common/src/main/java/io/xeres/common/protocol/ip/IP.java @@ -0,0 +1,346 @@ +/* + * Copyright (c) 2019-2020 by David Gerber - https://zapek.com + * + * This file is part of Xeres. + * + * Xeres is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Xeres is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Xeres. If not, see . + */ + +package io.xeres.common.protocol.ip; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.IOException; +import java.net.*; +import java.util.Iterator; +import java.util.Set; +import java.util.concurrent.ThreadLocalRandom; +import java.util.stream.IntStream; + +/** + * IP handling utility class. + */ +public final class IP +{ + private static final Logger log = LoggerFactory.getLogger(IP.class); + + // List of port to avoid picking up as default because of their popularity in a NAT setup. + // Xeres uses a range from 1025 to 32767. + // Note that some ports aren't really popular but they're scanned by default by some anti-virus. + private static final Set reservedPorts = Set.of( + 1080, // Socks proxy + 1194, // Open VPN + 1433, // MS SQL + 1701, // L2TP + 1723, // PPTP VPN + 1900, // SSDP + 2021, // FTP ALG + 2041, // Mail.ru + 2086, // GNUnet + 2375, // Docker + 2376, // Docker (SSL) + 3074, // XBox Live + 3128, // Default proxy + 3306, // MySQL + 3389, // Remote Desktop Protocol + 4242, // Quassel + 4444, // I2P Proxy + 4500, // IPSec + 5000, // Yahoo! + 5001, // Yahoo! + 5050, // Yahoo! + 5101, // Yahoo! + 5190, // ICQ + 5060, // Asterisk + 5061, // Asterisk (SSL) + 5222, // Jabber + 5223, // Jabber + 5269, // Jabber + 6667, // IRC + 6697, // IRCS + 6881, // Bittorrent + 6882, // Bittorrent + 6883, // Bittorrent + 6884, // Bittorrent + 6885, // Bittorrent + 6886, // Bittorrent + 6887, // Bittorrent + 6888, // Bittorrent + 6889, // Bittorrent + 7652, // I2P + 7653, // I2P + 7654, // I2P + 7900, // Many local tests + 8000, // Many local tests + 8080, // Many local tests + 8088, // Many local tests + 8888, // Many local tests + 9001, // Tor + 9030, // Tor + 11523 // No idea why Kaspersky scans this + ); + + private static final int BINDING_ATTEMPTS_MAX = 100; // After that many failed attempts, there must be something wrong + + private IP() + { + throw new UnsupportedOperationException("Utility class"); + } + + /** + * Finds a free local port. There's a built-in blacklist of commonly used ports which are avoided. + * + * @return a free local port + */ + public static int getFreeLocalPort() + { + int port; + var bindErrorDetector = 0; + + while (true) + { + // Avoid Ephemeral ports, see https://en.wikipedia.org/wiki/Ephemeral_port + port = ThreadLocalRandom.current().nextInt(1025, 32767); + + if (reservedPorts.contains(port)) + { + continue; + } + + try (var socket = new Socket()) + { + socket.bind(new InetSocketAddress("0.0.0.0", port)); + return port; + } + catch (IOException e) + { + if (bindErrorDetector > BINDING_ATTEMPTS_MAX) + { + throw new IllegalStateException("Failure to bind a local port. Check your network setup."); + } + bindErrorDetector++; + } + } + } + + /** + * Tries its best to get the public IP address, without requiring an external + * server. Should work at all times unless the host has no Internet access. + * + * @return the public IP address or null + */ + public static String getLocalIpAddress() + { + String ip; + + try (var socket = new DatagramSocket()) + { + socket.connect(InetAddress.getByName("1.1.1.1"), 10000); + ip = socket.getLocalAddress().getHostAddress(); + if (isRoutableIp(ip)) + { + log.debug("Own IP found using routing system: {}", ip); + return ip; + } + + // The above is reported to not work on OSX, if so, just scan all interfaces manually. + ip = findIpFromInterfaces(); + if (isRoutableIp(ip)) + { + log.debug("Own IP found by walking down interfaces: {}", ip); + return ip; + } + } + catch (UnknownHostException | SocketException e) + { + ip = null; + } + return ip; + } + + /** + * Checks if the IP address can be bound to (ie. a server can run on it). + * + * @param ip the IP address to check + * @return true if it's bindable + */ + public static boolean isBindableIp(String ip) + { + return isLanIp(ip) || isPublicIp(ip) || isLocalIp(ip); + } + + /** + * Checks if the IP address is routable, which means it's either a valid LAN address (ie. 192.168.1.4) or a public IP address. + * + * @param ip the IP address to check + * @return true if it's routable + */ + public static boolean isRoutableIp(String ip) + { + return isLanIp(ip) || isPublicIp(ip); + } + + /** + * Checks if the IP address if from a LAN (ie. a privately routable IP address, like 192.168.1.4 or 10.0.0.5). + * + * @param ip the IP address to check + * @return true if it's a LAN address + */ + public static boolean isLanIp(String ip) + { + try + { + return isLanAddress(InetAddress.getByName(ip)); + } + catch (UnknownHostException e) + { + return false; + } + } + + /** + * Checks if the IP address is a publicly routable IP address (ie. an IP that an Internet router will forward). + * + * @param ip the IP address to check + * @return true if it's a public IP address + */ + public static boolean isPublicIp(String ip) + { + try + { + return isPublicAddress(InetAddress.getByName(ip)); + } + catch (UnknownHostException e) + { + return false; + } + } + + /** + * Checks if the IP address is a local IP (localhost or link local). + * + * @param ip the IP address to check + * @return true if it's a local IP address + */ + public static boolean isLocalIp(String ip) + { + try + { + return isLocalAddress(InetAddress.getByName(ip)); + } + catch (UnknownHostException e) + { + return false; + } + } + + /** + * Try to find the local IP by iterating all interfaces.
+ * Note: this doesn't work in all cases (eg. if docker has some 10.0.75.1 then it might be + * picked up before the proper interface. + * + * @return the IP address if found, otherwise null + * @throws SocketException if there's a failure to get the interfaces + */ + private static String findIpFromInterfaces() throws SocketException + { + Iterator interfaces = NetworkInterface.getNetworkInterfaces().asIterator(); + while (interfaces.hasNext()) + { + var networkInterface = interfaces.next(); + if (networkInterface.isUp()) + { + Iterator addresses = networkInterface.getInetAddresses().asIterator(); + + while (addresses.hasNext()) + { + InetAddress address = addresses.next(); + + if (isRoutableAddress(address)) + { + log.debug("IP found using interface enumeration system: {}", address.getHostAddress()); + return address.getHostAddress(); + } + } + } + } + return null; + } + + private static boolean isRoutableAddress(InetAddress address) + { + return isLanAddress(address) || isPublicAddress(address); + } + + private static boolean isLanAddress(InetAddress address) + { + return address.isSiteLocalAddress(); + } + + private static boolean isPublicAddress(InetAddress address) + { + return !(isSpecifiedHostOnThisNetwork(address) || // 0.0.0.0 - 0.255.255.255 + address.isLoopbackAddress() || // 127.0.0.0 - 127.255.255.255 + address.isSiteLocalAddress() || // 10.0.0.0 - 10.255.255.255 | 172.16.0.0 - 172.31.255.255 | 192.0.0.0 - 192.0.0.255 + isSharedAddressSpace(address) || // 100.64.0.0 - 100.127.255.255 + address.isLinkLocalAddress() || // 169.254.0.0 - 169.254.255.255 + address.isMulticastAddress() || // 224.0.0.0 - 239.255.255.255 + isLimitedBroadcastAddress(address)); // 255.255.255.255 + } + + private static boolean isLocalAddress(InetAddress address) + { + return address.isLinkLocalAddress() || + address.isLoopbackAddress(); + } + + private static boolean isLimitedBroadcastAddress(InetAddress address) + { + // 255.255.255.255, see rfc6890 + return IntStream.of(3, 2, 1, 0).allMatch(i -> address.getAddress()[i] == -1); + } + + /** + * Check if an address is a current network (0.0.0.0/8). It must not be sent except as a source + * address as part of an initialization procedure by which the host learns its full IP address.
+ * Note: 0.0.0.0 (bind to any interface) is included as well. If you only need it, use {@link InetAddress#isAnyLocalAddress()} instead. + * + * @param address the address to test + * @return true if the address represents a current network + * @see rfc6890 and rfc1122 (section 3.2.1.3) + */ + private static boolean isSpecifiedHostOnThisNetwork(InetAddress address) + { + return address.getAddress()[0] == 0; + } + + /** + * Check if an address is in a shared address space (100.64.0.0/10), which is used when the ISP + * is using a carrier-grade NAT. This address cannot be reached from the public Internet directly. + * + * @param address the address to test + * @return true if in a shared address space + * @see rfc6598 + */ + private static boolean isSharedAddressSpace(InetAddress address) + { + return address.getAddress()[0] == 100 && Byte.toUnsignedInt(address.getAddress()[1]) >= 64 && Byte.toUnsignedInt(address.getAddress()[1]) < 128; + } + + public static boolean isValidPort(int port) + { + return port > 0 && port < 65536; + } +} diff --git a/common/src/main/java/io/xeres/common/protocol/ip/package-info.java b/common/src/main/java/io/xeres/common/protocol/ip/package-info.java new file mode 100644 index 000000000..6ebe80eab --- /dev/null +++ b/common/src/main/java/io/xeres/common/protocol/ip/package-info.java @@ -0,0 +1,23 @@ +/* + * Copyright (c) 2019-2020 by David Gerber - https://zapek.com + * + * This file is part of Xeres. + * + * Xeres is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Xeres is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Xeres. If not, see . + */ + +/** + * IP protocol support. Only IPv4 is supported for now. + */ +package io.xeres.common.protocol.ip; diff --git a/common/src/main/java/io/xeres/common/rest/PathConfig.java b/common/src/main/java/io/xeres/common/rest/PathConfig.java new file mode 100644 index 000000000..733ec7325 --- /dev/null +++ b/common/src/main/java/io/xeres/common/rest/PathConfig.java @@ -0,0 +1,36 @@ +/* + * Copyright (c) 2019-2021 by David Gerber - https://zapek.com + * + * This file is part of Xeres. + * + * Xeres is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Xeres is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Xeres. If not, see . + */ + +package io.xeres.common.rest; + +public final class PathConfig +{ + private PathConfig() + { + throw new UnsupportedOperationException("Utility class"); + } + + public static final String CONFIG_PATH = "/api/v1/config"; + public static final String PROFILES_PATH = "/api/v1/profiles"; + public static final String LOCATIONS_PATH = "/api/v1/locations"; + public static final String CONNECTIONS_PATH = "/api/v1/connections"; + public static final String NOTIFICATIONS_PATH = "/api/v1/notifications"; + public static final String CHAT_PATH = "/api/v1/chat"; + public static final String IDENTITY_PATH = "/api/v1/identity"; +} diff --git a/common/src/main/java/io/xeres/common/rest/chat/CreateChatRoomRequest.java b/common/src/main/java/io/xeres/common/rest/chat/CreateChatRoomRequest.java new file mode 100644 index 000000000..c3cf7e810 --- /dev/null +++ b/common/src/main/java/io/xeres/common/rest/chat/CreateChatRoomRequest.java @@ -0,0 +1,32 @@ +/* + * Copyright (c) 2019-2021 by David Gerber - https://zapek.com + * + * This file is part of Xeres. + * + * Xeres is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Xeres is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Xeres. If not, see . + */ + +package io.xeres.common.rest.chat; + +import javax.validation.constraints.NotNull; + +public record CreateChatRoomRequest( + @NotNull + String name, + + @NotNull + String topic +) +{ +} diff --git a/common/src/main/java/io/xeres/common/rest/config/HostnameResponse.java b/common/src/main/java/io/xeres/common/rest/config/HostnameResponse.java new file mode 100644 index 000000000..8e231148e --- /dev/null +++ b/common/src/main/java/io/xeres/common/rest/config/HostnameResponse.java @@ -0,0 +1,24 @@ +/* + * Copyright (c) 2019-2021 by David Gerber - https://zapek.com + * + * This file is part of Xeres. + * + * Xeres is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Xeres is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Xeres. If not, see . + */ + +package io.xeres.common.rest.config; + +public record HostnameResponse(String hostname) +{ +} diff --git a/common/src/main/java/io/xeres/common/rest/config/IpAddressRequest.java b/common/src/main/java/io/xeres/common/rest/config/IpAddressRequest.java new file mode 100644 index 000000000..30ba68b10 --- /dev/null +++ b/common/src/main/java/io/xeres/common/rest/config/IpAddressRequest.java @@ -0,0 +1,43 @@ +/* + * Copyright (c) 2019-2021 by David Gerber - https://zapek.com + * + * This file is part of Xeres. + * + * Xeres is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Xeres is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Xeres. If not, see . + */ + +package io.xeres.common.rest.config; + +import org.hibernate.validator.constraints.Range; + +import javax.validation.constraints.NotNull; + +public record IpAddressRequest( + @NotNull + String ip, + + @NotNull + @Range(min = 1, max = 65535) + Integer port +) +{ + @Override + public String toString() + { + return "IpAddressRequest{" + + "ip='" + ip + '\'' + + ", port=" + port + + '}'; + } +} diff --git a/common/src/main/java/io/xeres/common/rest/config/IpAddressResponse.java b/common/src/main/java/io/xeres/common/rest/config/IpAddressResponse.java new file mode 100644 index 000000000..eed8057be --- /dev/null +++ b/common/src/main/java/io/xeres/common/rest/config/IpAddressResponse.java @@ -0,0 +1,24 @@ +/* + * Copyright (c) 2019-2021 by David Gerber - https://zapek.com + * + * This file is part of Xeres. + * + * Xeres is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Xeres is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Xeres. If not, see . + */ + +package io.xeres.common.rest.config; + +public record IpAddressResponse(String ip, Integer port) +{ +} diff --git a/common/src/main/java/io/xeres/common/rest/config/OwnIdentityRequest.java b/common/src/main/java/io/xeres/common/rest/config/OwnIdentityRequest.java new file mode 100644 index 000000000..abd12d3c7 --- /dev/null +++ b/common/src/main/java/io/xeres/common/rest/config/OwnIdentityRequest.java @@ -0,0 +1,35 @@ +/* + * Copyright (c) 2019-2021 by David Gerber - https://zapek.com + * + * This file is part of Xeres. + * + * Xeres is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Xeres is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Xeres. If not, see . + */ + +package io.xeres.common.rest.config; + +import io.xeres.common.dto.identity.IdentityConstants; + +import javax.validation.constraints.NotNull; +import javax.validation.constraints.Size; + +public record OwnIdentityRequest( + @NotNull + @Size(min = IdentityConstants.NAME_LENGTH_MIN, max = IdentityConstants.NAME_LENGTH_MAX) + String name, + + boolean anonymous +) +{ +} diff --git a/common/src/main/java/io/xeres/common/rest/config/OwnLocationRequest.java b/common/src/main/java/io/xeres/common/rest/config/OwnLocationRequest.java new file mode 100644 index 000000000..99d6a6b3b --- /dev/null +++ b/common/src/main/java/io/xeres/common/rest/config/OwnLocationRequest.java @@ -0,0 +1,33 @@ +/* + * Copyright (c) 2019-2021 by David Gerber - https://zapek.com + * + * This file is part of Xeres. + * + * Xeres is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Xeres is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Xeres. If not, see . + */ + +package io.xeres.common.rest.config; + +import io.xeres.common.dto.location.LocationConstants; + +import javax.validation.constraints.NotNull; +import javax.validation.constraints.Size; + +public record OwnLocationRequest( + @NotNull + @Size(min = LocationConstants.NAME_LENGTH_MIN, max = LocationConstants.NAME_LENGTH_MAX) + String name +) +{ +} diff --git a/common/src/main/java/io/xeres/common/rest/config/OwnProfileRequest.java b/common/src/main/java/io/xeres/common/rest/config/OwnProfileRequest.java new file mode 100644 index 000000000..b8f1a371f --- /dev/null +++ b/common/src/main/java/io/xeres/common/rest/config/OwnProfileRequest.java @@ -0,0 +1,33 @@ +/* + * Copyright (c) 2019-2021 by David Gerber - https://zapek.com + * + * This file is part of Xeres. + * + * Xeres is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Xeres is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Xeres. If not, see . + */ + +package io.xeres.common.rest.config; + +import io.xeres.common.dto.profile.ProfileConstants; + +import javax.validation.constraints.NotNull; +import javax.validation.constraints.Size; + +public record OwnProfileRequest( + @NotNull + @Size(min = ProfileConstants.NAME_LENGTH_MIN, max = ProfileConstants.NAME_LENGTH_MAX) + String name +) +{ +} diff --git a/common/src/main/java/io/xeres/common/rest/config/UsernameResponse.java b/common/src/main/java/io/xeres/common/rest/config/UsernameResponse.java new file mode 100644 index 000000000..1ed01b729 --- /dev/null +++ b/common/src/main/java/io/xeres/common/rest/config/UsernameResponse.java @@ -0,0 +1,24 @@ +/* + * Copyright (c) 2019-2021 by David Gerber - https://zapek.com + * + * This file is part of Xeres. + * + * Xeres is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Xeres is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Xeres. If not, see . + */ + +package io.xeres.common.rest.config; + +public record UsernameResponse(String username) +{ +} diff --git a/common/src/main/java/io/xeres/common/rest/location/RSIdResponse.java b/common/src/main/java/io/xeres/common/rest/location/RSIdResponse.java new file mode 100644 index 000000000..401291021 --- /dev/null +++ b/common/src/main/java/io/xeres/common/rest/location/RSIdResponse.java @@ -0,0 +1,24 @@ +/* + * Copyright (c) 2019-2021 by David Gerber - https://zapek.com + * + * This file is part of Xeres. + * + * Xeres is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Xeres is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Xeres. If not, see . + */ + +package io.xeres.common.rest.location; + +public record RSIdResponse(String RSId) +{ +} diff --git a/common/src/main/java/io/xeres/common/rest/profile/CertificateRequest.java b/common/src/main/java/io/xeres/common/rest/profile/CertificateRequest.java new file mode 100644 index 000000000..0b9b1d2d9 --- /dev/null +++ b/common/src/main/java/io/xeres/common/rest/profile/CertificateRequest.java @@ -0,0 +1,33 @@ +/* + * Copyright (c) 2019-2021 by David Gerber - https://zapek.com + * + * This file is part of Xeres. + * + * Xeres is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Xeres is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Xeres. If not, see . + */ + +package io.xeres.common.rest.profile; + +import javax.validation.constraints.NotNull; +import javax.validation.constraints.Size; + +public record CertificateRequest( + @NotNull(message = "Missing certificate") + @Size(min = LENGTH_MIN, max = LENGTH_MAX) + String certificate +) +{ + private static final int LENGTH_MIN = 8; + private static final int LENGTH_MAX = 16384; +} diff --git a/common/src/main/java/io/xeres/common/util/NoSuppressedRunnable.java b/common/src/main/java/io/xeres/common/util/NoSuppressedRunnable.java new file mode 100644 index 000000000..1a97fabb5 --- /dev/null +++ b/common/src/main/java/io/xeres/common/util/NoSuppressedRunnable.java @@ -0,0 +1,50 @@ +/* + * Copyright (c) 2019-2021 by David Gerber - https://zapek.com + * + * This file is part of Xeres. + * + * Xeres is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Xeres is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Xeres. If not, see . + */ + +package io.xeres.common.util; + +import org.slf4j.LoggerFactory; + +/** + * This interface should be used instead of Runnable for executors so that any + * exception is printed. If it's a scheduled executor, it will also keep running. + *
+ * Example: + *

+ *     executorService.scheduleAtFixedRate((NoSuppressedRunnable) this::manageChatRooms, 10, 10, TimeUnit.SECONDS);
+ * 
+ */ +@FunctionalInterface +public interface NoSuppressedRunnable extends Runnable +{ + @Override + default void run() + { + try + { + doRun(); + } + catch (Exception e) + { + LoggerFactory.getLogger(NoSuppressedRunnable.class).error("Exception in executor: ", e); + } + } + + void doRun(); +} diff --git a/common/src/test/java/io/xeres/common/id/IdTest.java b/common/src/test/java/io/xeres/common/id/IdTest.java new file mode 100644 index 000000000..752d796f8 --- /dev/null +++ b/common/src/test/java/io/xeres/common/id/IdTest.java @@ -0,0 +1,122 @@ +/* + * Copyright (c) 2019-2021 by David Gerber - https://zapek.com + * + * This file is part of Xeres. + * + * Xeres is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Xeres is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Xeres. If not, see . + */ + +package io.xeres.common.id; + +import org.junit.jupiter.api.Test; + +import java.math.BigInteger; + +import static org.junit.jupiter.api.Assertions.assertArrayEquals; +import static org.junit.jupiter.api.Assertions.assertEquals; + +class IdTest +{ + @Test + void Id_ToString_FromBytes_OK() + { + var id = new BigInteger("13352839ab34093f", 16); + + String result = Id.toString(id.toByteArray()); + + assertEquals("13352839ab34093f", result); + } + + @Test + void Id_ToString_FromBytes_Null() + { + String result = Id.toString((byte[]) null); + + assertEquals("", result); + } + + @Test + void Id_ToString_FromBytes_Empty() + { + String result = Id.toString(new byte[0]); + + assertEquals("", result); + } + + @Test + void Id_ToBytes_FromString_OK() + { + var id = "e40f238ecb395023"; + + byte[] result = Id.toBytes(id); + + assertArrayEquals(new byte[]{(byte) 0xe4, 0xf, 0x23, (byte) 0x8e, (byte) 0xcb, 0x39, 0x50, 0x23}, result); + } + + @Test + void Id_ToBytes_FromString_Null() + { + byte[] result = Id.toBytes(null); + + assertArrayEquals(new byte[0], result); + } + + @Test + void Id_ToBytes_FromString_Empty() + { + byte[] result = Id.toBytes(""); + + assertArrayEquals(new byte[0], result); + } + + @Test + void Id_ToString_FromLong_OK() + { + long id = 0x2344ab38L; + + String result = Id.toString(id); + + assertEquals("2344AB38", result); + } + + @Test + void Id_ToString_FromIdentifier_OK() + { + GxsId gxsId = new GxsId(new byte[]{0x32, 0x5e, 0x38, 0x1, (byte) 0x98, (byte) 0x8a, 0x34, 0x73, 0x47, (byte) 0xef, 0x3e, 0x5a, (byte) 0xe2, 0x4a, 0x63, (byte) 0xba}); + + String result = Id.toString(gxsId); + + assertEquals("325e3801988a347347ef3e5ae24a63ba", result); + } + + @Test + void Id_AsciiToBytes_OK() + { + byte[] id = {0x30, 0x30, 0x35, 0x36, 0x33, 0x65, 0x38, 0x36, 0x61, 0x31, 0x64, 0x62, 0x36, 0x61, 0x61, 0x30, 0x32, 0x64, 0x36, 0x62, 0x36, 0x65, 0x38, 0x66, 0x37, 0x64, 0x61, 0x32, 0x62, 0x36, 0x39, 0x35}; + + byte[] result = Id.asciiToBytes(id); + + assertArrayEquals(new byte[]{0x0, 0x56, 0x3e, (byte) 0x86, (byte) 0xa1, (byte) 0xdb, 0x6a, (byte) 0xa0, 0x2d, 0x6b, 0x6e, (byte) 0x8f, 0x7d, (byte) 0xa2, (byte) 0xb6, (byte) 0x95}, result); + } + + @Test + void Id_IdentifierToAscii_OK() + { + GxsId gxsId = new GxsId(new byte[]{0x32, 0x5e, 0x38, 0x1, (byte) 0x98, (byte) 0x8a, 0x34, 0x73, 0x47, (byte) 0xef, 0x3e, 0x5a, (byte) 0xe2, 0x4a, 0x63, (byte) 0xba}); + + byte[] result = Id.toAsciiBytes(gxsId); + + assertArrayEquals(new byte[]{0x33, 0x32, 0x35, 0x65, 0x33, 0x38, 0x30, 0x31, 0x39, 0x38, 0x38, 0x61, 0x33, 0x34, 0x37, 0x33, 0x34, 0x37, 0x65, 0x66, 0x33, 0x65, 0x35, 0x61, 0x65, 0x32, 0x34, 0x61, 0x36, 0x33, 0x62, 0x61}, result); + } +} diff --git a/common/src/test/java/io/xeres/common/identity/TypeTest.java b/common/src/test/java/io/xeres/common/identity/TypeTest.java new file mode 100644 index 000000000..6b4ab9456 --- /dev/null +++ b/common/src/test/java/io/xeres/common/identity/TypeTest.java @@ -0,0 +1,36 @@ +/* + * Copyright (c) 2019-2021 by David Gerber - https://zapek.com + * + * This file is part of Xeres. + * + * Xeres is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Xeres is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Xeres. If not, see . + */ + +package io.xeres.common.identity; + +import org.junit.jupiter.api.Test; + +import static io.xeres.common.identity.Type.*; +import static org.junit.jupiter.api.Assertions.assertEquals; + +class TypeTest +{ + @Test + void Type_Enum_Order() + { + assertEquals(0, SIGNED.ordinal()); + assertEquals(1, ANONYMOUS.ordinal()); + assertEquals(2, FRIEND.ordinal()); + } +} diff --git a/common/src/test/java/io/xeres/common/pgp/TrustTest.java b/common/src/test/java/io/xeres/common/pgp/TrustTest.java new file mode 100644 index 000000000..d3bd82404 --- /dev/null +++ b/common/src/test/java/io/xeres/common/pgp/TrustTest.java @@ -0,0 +1,38 @@ +/* + * Copyright (c) 2019-2021 by David Gerber - https://zapek.com + * + * This file is part of Xeres. + * + * Xeres is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Xeres is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Xeres. If not, see . + */ + +package io.xeres.common.pgp; + +import org.junit.jupiter.api.Test; + +import static io.xeres.common.pgp.Trust.*; +import static org.junit.jupiter.api.Assertions.assertEquals; + +class TrustTest +{ + @Test + void Trust_Enum_Order() + { + assertEquals(0, UNKNOWN.ordinal()); + assertEquals(1, NEVER.ordinal()); + assertEquals(2, MARGINAL.ordinal()); + assertEquals(3, FULL.ordinal()); + assertEquals(4, ULTIMATE.ordinal()); + } +} diff --git a/common/src/test/java/io/xeres/common/protocol/ip/IPTest.java b/common/src/test/java/io/xeres/common/protocol/ip/IPTest.java new file mode 100644 index 000000000..b838274e7 --- /dev/null +++ b/common/src/test/java/io/xeres/common/protocol/ip/IPTest.java @@ -0,0 +1,102 @@ +/* + * Copyright (c) 2019-2021 by David Gerber - https://zapek.com + * + * This file is part of Xeres. + * + * Xeres is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Xeres is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Xeres. If not, see . + */ + +package io.xeres.common.protocol.ip; + +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.*; + +class IPTest +{ + @Test + void IP_GetFreeLocalPort_OK() + { + int port = IP.getFreeLocalPort(); + + assertTrue(port >= 1025 && port <= 32766); + } + + @Test + void IP_GetLocalIPAddress_OK() + { + String ip = IP.getLocalIpAddress(); + + assertNotNull(ip); + } + + @Test + void IP_IsLanIP_OK() + { + assertTrue(IP.isLanIp("10.0.0.0")); + assertTrue(IP.isLanIp("10.255.255.255")); + + assertTrue(IP.isLanIp("172.16.0.0")); + assertTrue(IP.isLanIp("172.31.255.255")); + + assertTrue(IP.isLanIp("192.168.0.0")); + assertTrue(IP.isLanIp("192.168.255.255")); + + assertTrue(IP.isLanIp("192.168.1.5")); + assertTrue(IP.isLanIp("172.16.0.5")); + assertTrue(IP.isLanIp("10.0.0.5")); + } + + @Test + void IP_IsLanIP_Fail() + { + assertFalse(IP.isLanIp("85.1.2.78")); + } + + @Test + void IP_IsLanIP_Empty_Fail() + { + assertFalse(IP.isLanIp("")); + } + + @Test + void IP_IsLanIP_Null_Fail() + { + assertFalse(IP.isLanIp(null)); + } + + @Test + void IP_IsPublicIP_OK() + { + assertTrue(IP.isPublicIp("85.1.2.78")); + } + + @Test + void IP_IsPublicIP_Fail() + { + assertFalse(IP.isPublicIp("192.168.1.5")); + } + + @Test + void IP_IsPublicIP_Empty_Fail() + { + assertFalse(IP.isPublicIp("")); + } + + @Test + void IP_IsPublicIP_Null_Fail() + { + assertFalse(IP.isPublicIp(null)); + } +} diff --git a/common/src/test/java/io/xeres/common/protocol/ip/NetModeTest.java b/common/src/test/java/io/xeres/common/protocol/ip/NetModeTest.java new file mode 100644 index 000000000..b56c6c990 --- /dev/null +++ b/common/src/test/java/io/xeres/common/protocol/ip/NetModeTest.java @@ -0,0 +1,39 @@ +/* + * Copyright (c) 2019-2021 by David Gerber - https://zapek.com + * + * This file is part of Xeres. + * + * Xeres is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Xeres is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Xeres. If not, see . + */ + +package io.xeres.common.protocol.ip; + +import org.junit.jupiter.api.Test; + +import static io.xeres.common.protocol.NetMode.*; +import static org.junit.jupiter.api.Assertions.assertEquals; + +class NetModeTest +{ + @Test + void NetMode_Enum_Order() + { + assertEquals(0, UNKNOWN.ordinal()); + assertEquals(1, UDP.ordinal()); + assertEquals(2, UPNP.ordinal()); + assertEquals(3, EXT.ordinal()); + assertEquals(4, HIDDEN.ordinal()); + assertEquals(5, UNREACHABLE.ordinal()); + } +} diff --git a/doc/manual.adoc b/doc/manual.adoc new file mode 100644 index 000000000..85b5150ab --- /dev/null +++ b/doc/manual.adoc @@ -0,0 +1,71 @@ += Just a test +:tabsize: 4 +:icons: font + +First let's start with some paragraph. + +I can also have one line in it. + +Then I can have paragraph literals + + Like this for example. It's emphasized :) + And I can continue + +NOTE: There are some common tips! + +TIP: this is a tip for testing. + +IMPORTANT: don't forget that you must do it! + +And now: this is some *bold* text, some _italic_ and `monospace` font too. + +`{cpp}` is valid syntax. + +== And this is a header + +And we can have some code examples directly from the source: + +[source,java] +---- +include::../app/src/test/java/io/xeres/app/crypto/x509/X509Test.java[tag=X509_GenerateCertificate_OK] +---- + +And another source code: + +[source,java] +---- +include::../app/src/test/java/io/xeres/app/crypto/rsa/RSATest.java[tag=RSA_GenerateKeys_OK] +---- + +* [x] this is ok +* [ ] this is not + + +Source code with callouts: + +[source,java] +---- +public class TestClass +{ + public void doStuff() + { + Foobar foobar = Foobar.generate(); // <1> + foobar.doThings(); // <2> + foobar.cleanup(); // <3> + } +} +---- + +<1> We start here +<2> Then we do the things +<3> And we cleanup + +.Sidebar content +**** +And this is a sidebar content +**** + +[quote,Max Payne,Max Payne 3] +____ +I had a hole in my second favourite drinking arm. +____ diff --git a/doc/services/chat.adoc b/doc/services/chat.adoc new file mode 100644 index 000000000..3a70e73b2 --- /dev/null +++ b/doc/services/chat.adoc @@ -0,0 +1,77 @@ += Chat +:icons: font + +Service type: 18 + +Is used to chat. +Contains distant chat, peer chat and distributed chat. + +== Items + +|=== +|Item | Subtype | Purpose + +|ChatMessage +|1 +|Contains a chat message + +|ChatAvatar +|3 +|Contains a JPEG image of the peer's avatar + +|ChatStatus +|4 +|Contains the chat status string (ie. `Foo is typing...`). +Sent at most every 5 seconds and only for private chats + +|PrivateChatMessageConfig +|5 +| + +|ChatRoomConnectChallenge +|9 +|Contains a connect challenge code. +Used to find if a peer is on the same private channel but also used for public ones. +If successful, a ChatRoomInvite is sent back. + +|ChatRoomUnsubscribe +|10 +| + +|ChatRoomListRequest +|13 +|Requests a ChatRoomList item. +Sent every 120 seconds + +|ChatRoomConfig +|21 +| + +|ChatRoomMessage +|23 +| + +|ChatRoomEvent +|24 +| + +|ChatRoomList +|25 +|Contains the list of rooms a peer is subscribed to + +|ChatRoomInvite +|27 +|Contains a chat room details. +Sent either when responding to a challenge or when needing to subscribe to an available room. +The flags are set accordingly + +|PrivateOutgoingMap +|28 +| + +|SubscribedChatRoomConfig +|29 +| + +|=== + diff --git a/doc/services/gxsid.adoc b/doc/services/gxsid.adoc new file mode 100644 index 000000000..982dd8537 --- /dev/null +++ b/doc/services/gxsid.adoc @@ -0,0 +1,5 @@ += GxSId +:icons: font + +Service type: 529 + diff --git a/doc/services/gxstrans.adoc b/doc/services/gxstrans.adoc new file mode 100644 index 000000000..5d1102161 --- /dev/null +++ b/doc/services/gxstrans.adoc @@ -0,0 +1,39 @@ += Gxs Transfers + +No service type + +This is used to transfer groups and messages between Gxs services. +All Gxs services are a sub-service of this one. + +== Items + +|=== +|Item | Subtype | Purpose + +|GxsSyncGroupRequestItem + +[small]#RsNxsSyncGrpReqItem# +|1 +|Requests a GxsSyncGroupItem. +Sent every 60 seconds. + +|GxsSyncGroupItem + +[small]#RsNxsSyncGrpItem# +|2 +|Contains a group id which have to be transferred. + +|GxsSyncGroupStatsItem + +[small]#RsNxsSyncGrpStatsItem# +|3 +|Requests or contains statistics about a group (number of posts and last timestamp). + +|GxsTransferGroupItem + +[small]#RsNxsGrp# +|4 +|Contains the Gxs group data. + +|GxsTransactionItem + +[small]#RsNxsTransacItem# +|64 +|Controls transactions which is a way to send a collection of items. + +|=== \ No newline at end of file diff --git a/doc/services/heartbeat.adoc b/doc/services/heartbeat.adoc new file mode 100644 index 000000000..89ae994b2 --- /dev/null +++ b/doc/services/heartbeat.adoc @@ -0,0 +1,21 @@ += Heartbeat +:icons: font + +Service type: 6 + +Sends a ping each 5 seconds. +Can be used to detect if a peer is gone. +Is redundant with the slice probe (1 minute) and RTT (10 seconds). + +NOTE: Retroshare sends a heartbeat to all peers every 5 seconds. +If an heartbeat is not received for 100 seconds, it checks if there was traffic and if there wasn't, it assumes the peer is dead. + +== Items + +|=== +|Item | Subtype | Purpose + +|Beat +|1 +|Tells the other peer that we are alive +|=== diff --git a/doc/services/rtt.adoc b/doc/services/rtt.adoc new file mode 100644 index 000000000..2c8ca8b8f --- /dev/null +++ b/doc/services/rtt.adoc @@ -0,0 +1,59 @@ += RTT +:icons: font + +Service type: 37 + +Sends a ping each 10 seconds. +Responds with a pong when a ping is received. +Allows to measure the times it takes to reach a peer, and back. + +== Items + +|=== +|Item | Subtype | Purpose + +|Ping +|1 +|Sent to the peer to receive a reply + +|Pong +|2 +|Sent as a reply to a ping item + +|=== + +== Fields + +=== Ping + +|=== +|Name | Type | Description + +|Sequence number +|int +|Is incremented for each new item sent. + +|Timestamp +|long +|Contains the current time when the item is sent. +This is a 64-bit timestamp which contains the number of seconds from 01-01-1970 on the 32-bit MSB part and the number of nanoseconds on the 32-bit LSB part. +|=== + +=== Pong + +|=== +|Name | Type | Description + +|Sequence number +|int +|The sequence number corresponding to the ping item. + +|Ping timestamp +|long +|The timestamp from the corresponding ping item. + +|Pong timestamp +|long + +|The current time when the item is sent. +|=== diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 000000000..463467264 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,13 @@ +version: '2.4' +services: + xeres: + image: zapek/xeres:0.1.0 + ports: + - "1067:1066" + - "3335:3335" + environment: + - SPRING_PROFILES_ACTIVE=cloud + - XERES_SERVER_PORT=3335 + - XERES_DATA_DIR=/tmp + - "JAVA_TOOL_OPTIONS=-Djava.net.preferIPv4Stack=true -Dfile.encoding=UTF-8" + mem_limit: 1G diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 0000000000000000000000000000000000000000..7454180f2ae8848c63b8b4dea2cb829da983f2fa GIT binary patch literal 59536 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 QuEJmfE|#`Dqx|px0L@K;Y5)KL literal 0 HcmV?d00001 diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 000000000..ffed3a254 --- /dev/null +++ b/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,5 @@ +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-7.2-bin.zip +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists diff --git a/gradlew b/gradlew new file mode 100644 index 000000000..744e882ed --- /dev/null +++ b/gradlew @@ -0,0 +1,185 @@ +#!/usr/bin/env sh + +# +# Copyright 2015 the original author or authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +############################################################################## +## +## Gradle start up script for UN*X +## +############################################################################## + +# Attempt to set APP_HOME +# Resolve links: $0 may be a link +PRG="$0" +# Need this for relative symlinks. +while [ -h "$PRG" ] ; do + ls=`ls -ld "$PRG"` + link=`expr "$ls" : '.*-> \(.*\)$'` + if expr "$link" : '/.*' > /dev/null; then + PRG="$link" + else + PRG=`dirname "$PRG"`"/$link" + fi +done +SAVED="`pwd`" +cd "`dirname \"$PRG\"`/" >/dev/null +APP_HOME="`pwd -P`" +cd "$SAVED" >/dev/null + +APP_NAME="Gradle" +APP_BASE_NAME=`basename "$0"` + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD="maximum" + +warn () { + echo "$*" +} + +die () { + echo + echo "$*" + echo + exit 1 +} + +# OS specific support (must be 'true' or 'false'). +cygwin=false +msys=false +darwin=false +nonstop=false +case "`uname`" in + CYGWIN* ) + cygwin=true + ;; + Darwin* ) + darwin=true + ;; + MSYS* | MINGW* ) + msys=true + ;; + NONSTOP* ) + nonstop=true + ;; +esac + +CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar + + +# Determine the Java command to use to start the JVM. +if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD="$JAVA_HOME/jre/sh/java" + else + JAVACMD="$JAVA_HOME/bin/java" + fi + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +else + JAVACMD="java" + which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." +fi + +# Increase the maximum file descriptors if we can. +if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then + MAX_FD_LIMIT=`ulimit -H -n` + if [ $? -eq 0 ] ; then + if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then + MAX_FD="$MAX_FD_LIMIT" + fi + ulimit -n $MAX_FD + if [ $? -ne 0 ] ; then + warn "Could not set maximum file descriptor limit: $MAX_FD" + fi + else + warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" + fi +fi + +# For Darwin, add options to specify how the application appears in the dock +if $darwin; then + GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" +fi + +# For Cygwin or MSYS, switch paths to Windows format before running java +if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then + APP_HOME=`cygpath --path --mixed "$APP_HOME"` + CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` + + JAVACMD=`cygpath --unix "$JAVACMD"` + + # We build the pattern for arguments to be converted via cygpath + ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` + SEP="" + for dir in $ROOTDIRSRAW ; do + ROOTDIRS="$ROOTDIRS$SEP$dir" + SEP="|" + done + OURCYGPATTERN="(^($ROOTDIRS))" + # Add a user-defined pattern to the cygpath arguments + if [ "$GRADLE_CYGPATTERN" != "" ] ; then + OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" + fi + # Now convert the arguments - kludge to limit ourselves to /bin/sh + i=0 + for arg in "$@" ; do + CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` + CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option + + if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition + eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` + else + eval `echo args$i`="\"$arg\"" + fi + i=`expr $i + 1` + done + case $i in + 0) set -- ;; + 1) set -- "$args0" ;; + 2) set -- "$args0" "$args1" ;; + 3) set -- "$args0" "$args1" "$args2" ;; + 4) set -- "$args0" "$args1" "$args2" "$args3" ;; + 5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; + 6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; + 7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; + 8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; + 9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; + esac +fi + +# Escape application args +save () { + for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done + echo " " +} +APP_ARGS=`save "$@"` + +# Collect all arguments for the java command, following the shell quoting and substitution rules +eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" + +exec "$JAVACMD" "$@" diff --git a/gradlew.bat b/gradlew.bat new file mode 100644 index 000000000..ac1b06f93 --- /dev/null +++ b/gradlew.bat @@ -0,0 +1,89 @@ +@rem +@rem Copyright 2015 the original author or authors. +@rem +@rem Licensed under the Apache License, Version 2.0 (the "License"); +@rem you may not use this file except in compliance with the License. +@rem You may obtain a copy of the License at +@rem +@rem https://www.apache.org/licenses/LICENSE-2.0 +@rem +@rem Unless required by applicable law or agreed to in writing, software +@rem distributed under the License is distributed on an "AS IS" BASIS, +@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +@rem See the License for the specific language governing permissions and +@rem limitations under the License. +@rem + +@if "%DEBUG%" == "" @echo off +@rem ########################################################################## +@rem +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables with windows NT shell +if "%OS%"=="Windows_NT" setlocal + +set DIRNAME=%~dp0 +if "%DIRNAME%" == "" set DIRNAME=. +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Resolve any "." and ".." in APP_HOME to make it shorter. +for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if "%ERRORLEVEL%" == "0" goto execute + +echo. +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto execute + +echo. +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:execute +@rem Setup the command line + +set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar + + +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* + +:end +@rem End local scope for the variables with windows NT shell +if "%ERRORLEVEL%"=="0" goto mainEnd + +:fail +rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of +rem the _cmd.exe /c_ return code! +if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 +exit /b 1 + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega diff --git a/icon.ico b/icon.ico new file mode 100644 index 0000000000000000000000000000000000000000..9a4d1d6f217321c11e30645c35650708fd38fde5 GIT binary patch literal 9062 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. + */ + +rootProject.name = 'Xeres' +include 'ui' +include 'app' +include 'webui' +include 'common' + diff --git a/ui/.gitignore b/ui/.gitignore new file mode 100644 index 000000000..e8f6f56e5 --- /dev/null +++ b/ui/.gitignore @@ -0,0 +1,2 @@ +/build/ +/out/ \ No newline at end of file diff --git a/ui/build.gradle b/ui/build.gradle new file mode 100644 index 000000000..739e0f9e5 --- /dev/null +++ b/ui/build.gradle @@ -0,0 +1,39 @@ +/* + * Copyright (c) 2019-2021 by David Gerber - https://zapek.com + * + * This file is part of Xeres. + * + * Xeres is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Xeres is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Xeres. If not, see . + */ + +plugins { + id 'org.openjfx.javafxplugin' version '0.0.10' +} + +editorconfig { + includes = ['src/**'] +} + +javafx { + version = "17" + modules = [ 'javafx.controls', 'javafx.fxml' ] +} + +dependencies { + implementation project(':common') + implementation 'org.springframework.boot:spring-boot-starter-webflux' // for the client + implementation 'org.springframework.boot:spring-boot-starter-websocket' + implementation 'net.rgielen:javafx-weaver-spring-boot-starter:1.3.0' + implementation "org.apache.commons:commons-lang3:$apacheCommonsLangVersion" +} diff --git a/ui/src/main/java/io/xeres/ui/JavaFxApplication.java b/ui/src/main/java/io/xeres/ui/JavaFxApplication.java new file mode 100644 index 000000000..f8c86b699 --- /dev/null +++ b/ui/src/main/java/io/xeres/ui/JavaFxApplication.java @@ -0,0 +1,99 @@ +/* + * Copyright (c) 2019-2021 by David Gerber - https://zapek.com + * + * This file is part of Xeres. + * + * Xeres is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Xeres is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Xeres. If not, see . + */ + +package io.xeres.ui; + +import javafx.application.Application; +import javafx.application.HostServices; +import javafx.application.Platform; +import javafx.stage.Stage; +import net.rgielen.fxweaver.core.FxWeaver; +import net.rgielen.fxweaver.spring.SpringFxWeaver; +import org.springframework.boot.builder.SpringApplicationBuilder; +import org.springframework.context.ConfigurableApplicationContext; +import org.springframework.context.annotation.Bean; + +public class JavaFxApplication extends Application +{ + private ConfigurableApplicationContext springContext; + + private static HostServices hostServices; + + private static Class springApplicationClass; + + static void start(Class springApplicationClass, String[] args) + { + JavaFxApplication.springApplicationClass = springApplicationClass; + Application.launch(JavaFxApplication.class, args); + } + + @Bean + public FxWeaver fxWeaver(ConfigurableApplicationContext applicationContext) + { + return new SpringFxWeaver(applicationContext); + } + + @Override + public void init() + { + springContext = new SpringApplicationBuilder() + .sources(springApplicationClass) + .run(getParameters().getRaw().toArray(new String[0])); + } + + @Override + public void stop() + { + springContext.close(); + Platform.exit(); + } + + @Override + public void start(Stage primaryStage) + { + hostServices = getHostServices(); + + springContext.publishEvent(new StageReadyEvent(primaryStage)); + } + + public static void openUrl(String url) + { + hostServices.showDocument(url); + } + + public static String getHostnameAndPort() + { + return getHostname() + ":" + getControlPort(); + } + + private static String getHostname() + { + return System.getProperty("xrs.ui.address", "localhost"); + } + + private static int getControlPort() + { + return Integer.parseInt(System.getProperty("xrs.ui.port", "1066")); + } + + public static String getControlUrl() // XXX: get rid of that thing, just use a properties + { + return "http://" + getHostnameAndPort(); + } +} diff --git a/ui/src/main/java/io/xeres/ui/PrimaryStageInitializer.java b/ui/src/main/java/io/xeres/ui/PrimaryStageInitializer.java new file mode 100644 index 000000000..38fe82ff6 --- /dev/null +++ b/ui/src/main/java/io/xeres/ui/PrimaryStageInitializer.java @@ -0,0 +1,73 @@ +/* + * Copyright (c) 2019-2021 by David Gerber - https://zapek.com + * + * This file is part of Xeres. + * + * Xeres is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Xeres is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Xeres. If not, see . + */ + +package io.xeres.ui; + +import io.xeres.ui.client.ProfileClient; +import io.xeres.ui.client.message.ChatFrameHandler; +import io.xeres.ui.client.message.MessageClient; +import io.xeres.ui.controller.chat.ChatViewController; +import io.xeres.ui.support.window.WindowManager; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.context.ApplicationListener; +import org.springframework.http.HttpStatus; +import org.springframework.stereotype.Component; +import org.springframework.web.reactive.function.client.WebClientResponseException; +import reactor.core.publisher.Hooks; + +import static io.xeres.common.rest.PathConfig.CHAT_PATH; + +@Component +public class PrimaryStageInitializer implements ApplicationListener +{ + private static final Logger log = LoggerFactory.getLogger(PrimaryStageInitializer.class); + + private final WindowManager windowManager; + private final ChatViewController chatViewController; + private final ProfileClient profileClient; + private final MessageClient messageClient; + + public PrimaryStageInitializer(WindowManager windowManager, ChatViewController chatViewController, ProfileClient profileClient, MessageClient messageClient) + { + this.windowManager = windowManager; + this.chatViewController = chatViewController; + this.profileClient = profileClient; + this.messageClient = messageClient; + } + + @Override + public void onApplicationEvent(StageReadyEvent event) + { + Hooks.onErrorDropped(throwable -> log.debug("WebClient warning: {}", throwable.getMessage())); // Suppress Reactor's error messages + + profileClient.getOwnProfile() + .doOnSuccess(profile -> windowManager.openMain(event.getStage())) + .doOnError(WebClientResponseException.class, e -> { + if (e.getStatusCode() == HttpStatus.NOT_FOUND) + { + windowManager.openAccountCreation(event.getStage()); + } + }) // XXX: try finding out what happens if ie. the host is unreachable (connecting to unknown host or so). we probably need some other doOnError() + .subscribe(); + + messageClient.subscribe(CHAT_PATH, new ChatFrameHandler(windowManager, chatViewController)); + messageClient.connect(); // XXX: not so nice here. what about future subscription systems? + } +} diff --git a/ui/src/main/java/io/xeres/ui/StageReadyEvent.java b/ui/src/main/java/io/xeres/ui/StageReadyEvent.java new file mode 100644 index 000000000..238dfc50a --- /dev/null +++ b/ui/src/main/java/io/xeres/ui/StageReadyEvent.java @@ -0,0 +1,39 @@ +/* + * Copyright (c) 2019-2020 by David Gerber - https://zapek.com + * + * This file is part of Xeres. + * + * Xeres is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Xeres is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Xeres. If not, see . + */ + +package io.xeres.ui; + +import javafx.stage.Stage; +import org.springframework.context.ApplicationEvent; + +public class StageReadyEvent extends ApplicationEvent +{ + private final Stage stage; + + public StageReadyEvent(Stage primaryStage) + { + super(primaryStage); + this.stage = primaryStage; + } + + public Stage getStage() + { + return stage; + } +} diff --git a/ui/src/main/java/io/xeres/ui/UiStarter.java b/ui/src/main/java/io/xeres/ui/UiStarter.java new file mode 100644 index 000000000..79febd332 --- /dev/null +++ b/ui/src/main/java/io/xeres/ui/UiStarter.java @@ -0,0 +1,33 @@ +/* + * Copyright (c) 2019-2020 by David Gerber - https://zapek.com + * + * This file is part of Xeres. + * + * Xeres is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Xeres is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Xeres. If not, see . + */ + +package io.xeres.ui; + +public final class UiStarter +{ + private UiStarter() + { + throw new UnsupportedOperationException("Utility class"); + } + + public static void start(Class springApplicationClass, String[] args) + { + JavaFxApplication.start(springApplicationClass, args); + } +} diff --git a/ui/src/main/java/io/xeres/ui/client/ChatClient.java b/ui/src/main/java/io/xeres/ui/client/ChatClient.java new file mode 100644 index 000000000..35f3f481d --- /dev/null +++ b/ui/src/main/java/io/xeres/ui/client/ChatClient.java @@ -0,0 +1,82 @@ +/* + * Copyright (c) 2019-2021 by David Gerber - https://zapek.com + * + * This file is part of Xeres. + * + * Xeres is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Xeres is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Xeres. If not, see . + */ + +package io.xeres.ui.client; + +import io.xeres.common.rest.chat.CreateChatRoomRequest; +import io.xeres.ui.JavaFxApplication; +import org.springframework.stereotype.Component; +import org.springframework.web.reactive.function.client.WebClient; +import reactor.core.publisher.Mono; + +import javax.annotation.PostConstruct; + +import static io.xeres.common.rest.PathConfig.CHAT_PATH; + +@Component +public class ChatClient +{ + private final WebClient.Builder webClientBuilder; + + private WebClient webClient; + + public ChatClient(WebClient.Builder webClientBuilder) + { + this.webClientBuilder = webClientBuilder; + } + + @PostConstruct + private void init() + { + webClient = webClientBuilder + .baseUrl(JavaFxApplication.getControlUrl() + CHAT_PATH) + .build(); + } + + public Mono createChatRoom(String name, String topic) + { + var request = new CreateChatRoomRequest(name, topic); + + return webClient.post() + .uri("/rooms") + .bodyValue(request) + .retrieve() + .bodyToMono(Void.class); + } + + public Mono joinChatRoom(long id) + { + return webClient.put() + .uri(uriBuilder -> uriBuilder + .path("/rooms/{id}/subscription") + .build(id)) + .retrieve() + .bodyToMono(Long.class); + } + + public Mono leaveChatRoom(long id) + { + return webClient.delete() + .uri(uriBuilder -> uriBuilder + .path("/rooms/{id}/subscription") + .build(id)) + .retrieve() + .bodyToMono(Void.class); + } +} diff --git a/ui/src/main/java/io/xeres/ui/client/ConfigClient.java b/ui/src/main/java/io/xeres/ui/client/ConfigClient.java new file mode 100644 index 000000000..0f42ad21d --- /dev/null +++ b/ui/src/main/java/io/xeres/ui/client/ConfigClient.java @@ -0,0 +1,111 @@ +/* + * Copyright (c) 2019-2021 by David Gerber - https://zapek.com + * + * This file is part of Xeres. + * + * Xeres is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Xeres is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Xeres. If not, see . + */ + +package io.xeres.ui.client; + +import io.xeres.common.rest.config.*; +import io.xeres.ui.JavaFxApplication; +import org.springframework.stereotype.Component; +import org.springframework.web.reactive.function.client.WebClient; +import reactor.core.publisher.Mono; + +import javax.annotation.PostConstruct; + +import static io.xeres.common.rest.PathConfig.CONFIG_PATH; + +@Component +public class ConfigClient +{ + private final WebClient.Builder webClientBuilder; + + private WebClient webClient; + + public ConfigClient(WebClient.Builder webClientBuilder) + { + this.webClientBuilder = webClientBuilder; + } + + @PostConstruct + private void init() + { + webClient = webClientBuilder + .baseUrl(JavaFxApplication.getControlUrl() + CONFIG_PATH) + .build(); + } + + public Mono createProfile(String name) + { + var profileRequest = new OwnProfileRequest(name); + + return webClient.post() + .uri("/profile") + .bodyValue(profileRequest) + .retrieve() + .bodyToMono(Void.class); + } + + public Mono createLocation(String name) + { + var locationRequest = new OwnLocationRequest(name); + + return webClient.post() + .uri("/location") + .bodyValue(locationRequest) + .retrieve() + .bodyToMono(Void.class); + } + + public Mono createIdentity(String name, boolean anonymous) + { + var identityRequest = new OwnIdentityRequest(name, anonymous); + + return webClient.post() + .uri("/identity") + .bodyValue(identityRequest) + .retrieve() + .bodyToMono(Void.class); + } + + public Mono updateExternalIpAddress(String ip, int port) + { + var externalIpAddressRequest = new IpAddressRequest(ip, port); + + return webClient.put() + .uri("/externalIp") + .bodyValue(externalIpAddressRequest) + .retrieve() + .bodyToMono(Void.class); + } + + public Mono getHostname() + { + return webClient.get() + .uri("/hostname") + .retrieve() + .bodyToMono(HostnameResponse.class); + } + + public Mono getUsername() + { + return webClient.get() + .uri("/username") + .retrieve() + .bodyToMono(UsernameResponse.class); + } +} diff --git a/ui/src/main/java/io/xeres/ui/client/ConnectionClient.java b/ui/src/main/java/io/xeres/ui/client/ConnectionClient.java new file mode 100644 index 000000000..15c5374a4 --- /dev/null +++ b/ui/src/main/java/io/xeres/ui/client/ConnectionClient.java @@ -0,0 +1,62 @@ +/* + * Copyright (c) 2019-2021 by David Gerber - https://zapek.com + * + * This file is part of Xeres. + * + * Xeres is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Xeres is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Xeres. If not, see . + */ + +package io.xeres.ui.client; + +import io.xeres.common.dto.profile.ProfileDTO; +import io.xeres.ui.JavaFxApplication; +import io.xeres.ui.model.profile.Profile; +import io.xeres.ui.model.profile.ProfileMapper; +import org.springframework.stereotype.Component; +import org.springframework.web.reactive.function.client.WebClient; +import reactor.core.publisher.Flux; + +import javax.annotation.PostConstruct; + +import static io.xeres.common.rest.PathConfig.CONNECTIONS_PATH; + +@Component +public class ConnectionClient +{ + private final WebClient.Builder webClientBuilder; + + private WebClient webClient; + + public ConnectionClient(WebClient.Builder webClientBuilder) + { + this.webClientBuilder = webClientBuilder; + } + + @PostConstruct + private void init() + { + webClient = webClientBuilder + .baseUrl(JavaFxApplication.getControlUrl() + CONNECTIONS_PATH) + .build(); + } + + public Flux getConnectedProfiles() + { + return webClient.get() + .uri("/profiles") + .retrieve() + .bodyToFlux(ProfileDTO.class) + .map(ProfileMapper::fromDeepDTO); + } +} diff --git a/ui/src/main/java/io/xeres/ui/client/LocationClient.java b/ui/src/main/java/io/xeres/ui/client/LocationClient.java new file mode 100644 index 000000000..8af1b60fa --- /dev/null +++ b/ui/src/main/java/io/xeres/ui/client/LocationClient.java @@ -0,0 +1,72 @@ +/* + * Copyright (c) 2019-2021 by David Gerber - https://zapek.com + * + * This file is part of Xeres. + * + * Xeres is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Xeres is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Xeres. If not, see . + */ + +package io.xeres.ui.client; + +import io.xeres.common.dto.location.LocationDTO; +import io.xeres.common.id.LocationId; +import io.xeres.common.rest.location.RSIdResponse; +import io.xeres.ui.JavaFxApplication; +import io.xeres.ui.model.location.Location; +import io.xeres.ui.model.location.LocationMapper; +import org.springframework.stereotype.Component; +import org.springframework.web.reactive.function.client.WebClient; +import reactor.core.publisher.Mono; + +import javax.annotation.PostConstruct; + +import static io.xeres.common.rest.PathConfig.LOCATIONS_PATH; + +@Component +public class LocationClient +{ + private final WebClient.Builder webClientBuilder; + + private WebClient webClient; + + public LocationClient(WebClient.Builder webClientBuilder) + { + this.webClientBuilder = webClientBuilder; + } + + @PostConstruct + private void init() + { + webClient = webClientBuilder + .baseUrl(JavaFxApplication.getControlUrl() + LOCATIONS_PATH) + .build(); + } + + public Mono findById(LocationId locationId) + { + return webClient.get() + .uri("/{id}", locationId.toString()) + .retrieve() + .bodyToMono(LocationDTO.class) + .map(LocationMapper::fromDTO); + } + + public Mono getRSId(long id) + { + return webClient.get() + .uri("/{id}/rsid", id) + .retrieve() + .bodyToMono(RSIdResponse.class); + } +} diff --git a/ui/src/main/java/io/xeres/ui/client/ProfileClient.java b/ui/src/main/java/io/xeres/ui/client/ProfileClient.java new file mode 100644 index 000000000..563a89584 --- /dev/null +++ b/ui/src/main/java/io/xeres/ui/client/ProfileClient.java @@ -0,0 +1,115 @@ +/* + * Copyright (c) 2019-2021 by David Gerber - https://zapek.com + * + * This file is part of Xeres. + * + * Xeres is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Xeres is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Xeres. If not, see . + */ + +package io.xeres.ui.client; + +import io.xeres.common.dto.profile.ProfileDTO; +import io.xeres.common.id.LocationId; +import io.xeres.common.rest.profile.CertificateRequest; +import io.xeres.ui.JavaFxApplication; +import io.xeres.ui.model.profile.Profile; +import io.xeres.ui.model.profile.ProfileMapper; +import org.springframework.stereotype.Component; +import org.springframework.web.reactive.function.client.WebClient; +import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; + +import javax.annotation.PostConstruct; + +import static io.xeres.common.dto.profile.ProfileConstants.OWN_PROFILE_ID; +import static io.xeres.common.rest.PathConfig.PROFILES_PATH; + +@Component +public class ProfileClient +{ + private final WebClient.Builder webClientBuilder; + + private WebClient webClient; + + public ProfileClient(WebClient.Builder webClientBuilder) + { + this.webClientBuilder = webClientBuilder; + } + + @PostConstruct + private void init() + { + webClient = webClientBuilder + .baseUrl(JavaFxApplication.getControlUrl() + PROFILES_PATH) + .build(); + } + + public Mono createProfile(String certificate) + { + var certificateRequest = new CertificateRequest(certificate); + + return webClient.post() + .uri("/") + .bodyValue(certificateRequest) + .retrieve() + .bodyToMono(Void.class); + } + + public Flux getProfiles() + { + return webClient.get() + .uri("/") + .retrieve() + .bodyToFlux(ProfileDTO.class) + .map(ProfileMapper::fromDTO); + } + + public Mono getOwnProfile() + { + return findById(OWN_PROFILE_ID); + } + + public Mono checkCertificate(String certificate) + { + var certificateRequest = new CertificateRequest(certificate); + + return webClient.post() + .uri("/check") + .bodyValue(certificateRequest) + .retrieve() + .bodyToMono(ProfileDTO.class) + .map(ProfileMapper::fromDeepDTO); + } + + public Mono findById(long id) + { + return webClient.get() + .uri("/{id}", id) + .retrieve() + .bodyToMono(ProfileDTO.class) + .map(ProfileMapper::fromDeepDTO); + } + + public Flux findByLocationId(LocationId locationId) + { + return webClient.get() + .uri(uriBuilder -> uriBuilder + .path("/") + .queryParam("locationId", locationId.toString()) + .build()) + .retrieve() + .bodyToFlux(ProfileDTO.class) + .map(ProfileMapper::fromDeepDTO); + } +} diff --git a/ui/src/main/java/io/xeres/ui/client/message/ChatFrameHandler.java b/ui/src/main/java/io/xeres/ui/client/message/ChatFrameHandler.java new file mode 100644 index 000000000..3bbd09c69 --- /dev/null +++ b/ui/src/main/java/io/xeres/ui/client/message/ChatFrameHandler.java @@ -0,0 +1,89 @@ +/* + * Copyright (c) 2019-2021 by David Gerber - https://zapek.com + * + * This file is part of Xeres. + * + * Xeres is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Xeres is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Xeres. If not, see . + */ + +package io.xeres.ui.client.message; + +import io.xeres.common.message.MessageType; +import io.xeres.common.message.chat.ChatMessage; +import io.xeres.common.message.chat.ChatRoomListMessage; +import io.xeres.common.message.chat.ChatRoomMessage; +import io.xeres.ui.controller.chat.ChatViewController; +import io.xeres.ui.support.window.WindowManager; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.messaging.simp.stomp.StompFrameHandler; +import org.springframework.messaging.simp.stomp.StompHeaders; + +import java.lang.reflect.Type; +import java.util.Objects; + +import static io.xeres.common.message.MessageHeaders.DESTINATION_ID; +import static io.xeres.common.message.MessageHeaders.MESSAGE_TYPE; + +/** + * This handles the incoming messages from the server to the UI. + */ +public class ChatFrameHandler implements StompFrameHandler +{ + private static final Logger log = LoggerFactory.getLogger(ChatFrameHandler.class); + + private final WindowManager windowManager; + private final ChatViewController chatViewController; + + public ChatFrameHandler(WindowManager windowManager, ChatViewController chatViewController) + { + this.windowManager = windowManager; + this.chatViewController = chatViewController; + } + + @Override + public Type getPayloadType(StompHeaders headers) + { + var messageType = MessageType.valueOf(headers.getFirst(MESSAGE_TYPE)); + return switch (messageType) + { + case CHAT_PRIVATE_MESSAGE, CHAT_TYPING_NOTIFICATION -> ChatMessage.class; + case CHAT_ROOM_JOIN, CHAT_ROOM_LEAVE, CHAT_ROOM_MESSAGE -> ChatRoomMessage.class; + case CHAT_ROOM_LIST -> ChatRoomListMessage.class; + default -> throw new IllegalArgumentException("Missing class for message type " + messageType); + }; + } + + @Override + public void handleFrame(StompHeaders headers, Object payload) + { + var messageType = MessageType.valueOf(headers.getFirst(MESSAGE_TYPE)); + switch (messageType) + { + case CHAT_PRIVATE_MESSAGE, CHAT_TYPING_NOTIFICATION -> windowManager.openMessaging(headers.getFirst(DESTINATION_ID), (ChatMessage) payload); + case CHAT_ROOM_MESSAGE -> chatViewController.showMessage(getChatRoomMessage(headers, payload)); + case CHAT_ROOM_JOIN -> chatViewController.roomJoined(getChatRoomMessage(headers, payload).getRoomId()); + case CHAT_ROOM_LEAVE -> chatViewController.roomLeft(getChatRoomMessage(headers, payload).getRoomId()); + case CHAT_ROOM_LIST -> chatViewController.addRooms(((ChatRoomListMessage) payload).getRooms()); + default -> log.error("Missing handling of {}", messageType); + } + } + + private ChatRoomMessage getChatRoomMessage(StompHeaders headers, Object payload) + { + var chatRoomMessage = (ChatRoomMessage) payload; + chatRoomMessage.setRoomId(Long.parseLong(Objects.requireNonNull(headers.getFirst(DESTINATION_ID)))); + return chatRoomMessage; + } +} diff --git a/ui/src/main/java/io/xeres/ui/client/message/MessageClient.java b/ui/src/main/java/io/xeres/ui/client/message/MessageClient.java new file mode 100644 index 000000000..900d82911 --- /dev/null +++ b/ui/src/main/java/io/xeres/ui/client/message/MessageClient.java @@ -0,0 +1,151 @@ +/* + * Copyright (c) 2019-2021 by David Gerber - https://zapek.com + * + * This file is part of Xeres. + * + * Xeres is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Xeres is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Xeres. If not, see . + */ + +package io.xeres.ui.client.message; + +import io.xeres.common.id.LocationId; +import io.xeres.common.message.chat.ChatMessage; +import io.xeres.ui.JavaFxApplication; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.messaging.converter.MappingJackson2MessageConverter; +import org.springframework.messaging.simp.stomp.StompFrameHandler; +import org.springframework.messaging.simp.stomp.StompHeaders; +import org.springframework.messaging.simp.stomp.StompSession; +import org.springframework.stereotype.Component; +import org.springframework.util.concurrent.ListenableFuture; +import org.springframework.web.socket.client.WebSocketClient; +import org.springframework.web.socket.client.standard.StandardWebSocketClient; +import org.springframework.web.socket.messaging.WebSocketStompClient; + +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.ExecutionException; + +import static io.xeres.common.message.MessageHeaders.DESTINATION_ID; +import static io.xeres.common.message.MessageHeaders.MESSAGE_TYPE; +import static io.xeres.common.message.MessageType.*; +import static io.xeres.common.rest.PathConfig.CHAT_PATH; + +/** + * This sends messages to the server. + */ +@Component +public class MessageClient +{ + private static final Logger log = LoggerFactory.getLogger(MessageClient.class); + + private WebSocketStompClient stompClient; + + private ListenableFuture future; + + private StompSession stompSession; + + private final List pendingSubscriptions = new ArrayList<>(); + private final List subscriptions = new ArrayList<>(); + + public MessageClient connect() + { + String url = "ws://" + JavaFxApplication.getHostnameAndPort() + "/ws"; + + WebSocketClient client = new StandardWebSocketClient(); + stompClient = new WebSocketStompClient(client); + stompClient.setMessageConverter(new MappingJackson2MessageConverter()); + + var sessionHandler = new SessionHandler(session -> + { + stompSession = session; + performPendingSubscriptions(stompSession); + }); + + log.debug("Connecting to {}", url); + future = stompClient.connect(url, sessionHandler); + + return this; + } + + public void subscribe(String path, StompFrameHandler frameHandler) + { + pendingSubscriptions.add(new PendingSubscription(path, frameHandler)); + + if (stompSession != null) + { + performPendingSubscriptions(stompSession); + } + } + + public void sendToLocation(LocationId locationId, ChatMessage message) + { + assert stompSession != null; + + var headers = new StompHeaders(); + headers.setDestination("/app" + CHAT_PATH); + headers.set(MESSAGE_TYPE, message.isEmpty() ? CHAT_TYPING_NOTIFICATION.name() : CHAT_PRIVATE_MESSAGE.name()); + headers.set(DESTINATION_ID, locationId.toString()); + stompSession.send(headers, message); + } + + public void sendToChatRoom(long chatRoomId, ChatMessage message) + { + assert stompSession != null; + + var headers = new StompHeaders(); + headers.setDestination("/app" + CHAT_PATH); + headers.set(MESSAGE_TYPE, message.isEmpty() ? CHAT_ROOM_TYPING_NOTIFICATION.name() : CHAT_ROOM_MESSAGE.name()); + headers.set(DESTINATION_ID, String.valueOf(chatRoomId)); + stompSession.send(headers, message); + } + + public void sendBroadcast(ChatMessage message) + { + assert stompSession != null; + + var headers = new StompHeaders(); + headers.setDestination("/app" + CHAT_PATH); + headers.set(MESSAGE_TYPE, CHAT_BROADCAST_MESSAGE.name()); + stompSession.send(headers, message); + } + + private void performPendingSubscriptions(StompSession session) + { + while (!pendingSubscriptions.isEmpty()) + { + var pendingSubscription = pendingSubscriptions.remove(0); + + var subscription = session.subscribe(pendingSubscription.getPath(), pendingSubscription.getStompFrameHandler()); + subscriptions.add(subscription); + } + } + + // XXX: add unsubscribe()? how? + + // XXX: is the following needed still? + public void disconnect() + { + try + { + future.get().disconnect(); + } + catch (InterruptedException | ExecutionException e) + { + log.error("Error: {}", e.getMessage()); + } + stompClient.stop(); // XXX: not sure this is needed actually... + } +} diff --git a/ui/src/main/java/io/xeres/ui/client/message/PendingSubscription.java b/ui/src/main/java/io/xeres/ui/client/message/PendingSubscription.java new file mode 100644 index 000000000..dc58fdf12 --- /dev/null +++ b/ui/src/main/java/io/xeres/ui/client/message/PendingSubscription.java @@ -0,0 +1,44 @@ +/* + * Copyright (c) 2019-2020 by David Gerber - https://zapek.com + * + * This file is part of Xeres. + * + * Xeres is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Xeres is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Xeres. If not, see . + */ + +package io.xeres.ui.client.message; + +import org.springframework.messaging.simp.stomp.StompFrameHandler; + +public class PendingSubscription +{ + private final String path; + private final StompFrameHandler stompFrameHandler; + + PendingSubscription(String path, StompFrameHandler stompFrameHandler) + { + this.path = path; + this.stompFrameHandler = stompFrameHandler; + } + + public String getPath() + { + return path; + } + + public StompFrameHandler getStompFrameHandler() + { + return stompFrameHandler; + } +} diff --git a/ui/src/main/java/io/xeres/ui/client/message/SessionHandler.java b/ui/src/main/java/io/xeres/ui/client/message/SessionHandler.java new file mode 100644 index 000000000..3c428c9b2 --- /dev/null +++ b/ui/src/main/java/io/xeres/ui/client/message/SessionHandler.java @@ -0,0 +1,45 @@ +/* + * Copyright (c) 2019-2020 by David Gerber - https://zapek.com + * + * This file is part of Xeres. + * + * Xeres is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Xeres is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Xeres. If not, see . + */ + +package io.xeres.ui.client.message; + +import org.springframework.messaging.simp.stomp.StompHeaders; +import org.springframework.messaging.simp.stomp.StompSession; +import org.springframework.messaging.simp.stomp.StompSessionHandlerAdapter; + +public class SessionHandler extends StompSessionHandlerAdapter +{ + public interface OnConnected + { + void connect(StompSession session); + } + + private final OnConnected onConnected; + + SessionHandler(OnConnected onConnected) + { + this.onConnected = onConnected; + } + + @Override + public void afterConnected(StompSession session, StompHeaders connectedHeaders) + { + onConnected.connect(session); + } +} diff --git a/ui/src/main/java/io/xeres/ui/controller/Controller.java b/ui/src/main/java/io/xeres/ui/controller/Controller.java new file mode 100644 index 000000000..9a35babcd --- /dev/null +++ b/ui/src/main/java/io/xeres/ui/controller/Controller.java @@ -0,0 +1,30 @@ +/* + * Copyright (c) 2019-2021 by David Gerber - https://zapek.com + * + * This file is part of Xeres. + * + * Xeres is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Xeres is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Xeres. If not, see . + */ + +package io.xeres.ui.controller; + +import java.io.IOException; + +/** + * Use this interface when building a controller. If you need a Window, use WindowController instead. + */ +public interface Controller +{ + void initialize() throws IOException; +} diff --git a/ui/src/main/java/io/xeres/ui/controller/MainWindowController.java b/ui/src/main/java/io/xeres/ui/controller/MainWindowController.java new file mode 100644 index 000000000..fc6dd5bcf --- /dev/null +++ b/ui/src/main/java/io/xeres/ui/controller/MainWindowController.java @@ -0,0 +1,143 @@ +/* + * Copyright (c) 2019-2021 by David Gerber - https://zapek.com + * + * This file is part of Xeres. + * + * Xeres is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Xeres is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Xeres. If not, see . + */ + +package io.xeres.ui.controller; + +import io.xeres.common.rest.location.RSIdResponse; +import io.xeres.ui.JavaFxApplication; +import io.xeres.ui.client.LocationClient; +import io.xeres.ui.support.tray.TrayService; +import io.xeres.ui.support.util.UiUtils; +import io.xeres.ui.support.window.WindowManager; +import javafx.application.Platform; +import javafx.fxml.FXML; +import javafx.scene.control.Label; +import javafx.scene.control.MenuItem; +import javafx.scene.input.Clipboard; +import javafx.scene.input.ClipboardContent; +import javafx.stage.Stage; +import net.rgielen.fxweaver.core.FxmlView; +import org.springframework.stereotype.Component; +import reactor.core.publisher.Mono; + +import static io.xeres.common.dto.location.LocationConstants.OWN_LOCATION_ID; + +@Component +@FxmlView(value = "/view/main.fxml") +public class MainWindowController implements WindowController +{ + @FXML + private Label titleLabel; + + @FXML + private MenuItem addFriend; + + @FXML + private MenuItem copyOwnId; + + @FXML + private MenuItem launchWebInterface; + + @FXML + private MenuItem exitApplication; + + @FXML + private MenuItem createChatRoom; + + @FXML + private MenuItem showAboutWindow; + + @FXML + private MenuItem showProfilesWindow; + + @FXML + private MenuItem showFriendsWindow; + + @FXML + private MenuItem showBroadcastWindow; + + private final LocationClient locationClient; + private final TrayService trayService; + private final WindowManager windowManager; + + public MainWindowController(LocationClient locationClient, TrayService trayService, WindowManager windowManager) + { + this.locationClient = locationClient; + this.trayService = trayService; + this.windowManager = windowManager; + } + + public void initialize() + { + // Friends + addFriend.setOnAction(event -> addFriend()); + copyOwnId.setOnAction(event -> copyOwnId()); + + launchWebInterface.setOnAction(event -> launchBrowser()); + + createChatRoom.setOnAction(event -> windowManager.openChatRoomCreation(titleLabel.getScene().getWindow())); + + showAboutWindow.setOnAction(event -> windowManager.openAbout(titleLabel.getScene().getWindow())); + + showBroadcastWindow.setOnAction(event -> windowManager.openBroadcast(titleLabel.getScene().getWindow())); + + showProfilesWindow.setOnAction(event -> windowManager.openProfiles(titleLabel.getScene().getWindow())); + + showFriendsWindow.setOnAction(event -> windowManager.openFriends()); + + exitApplication.setOnAction(event -> + { + UiUtils.closeWindow(titleLabel); // the event can contain a MenuItem which is not a Node + + windowManager.closeAllWindows(); + + if (trayService.hasSystemTray()) + { + Platform.exit(); + } + }); + } + + @Override + public void onShown() + { + trayService.addSystemTray((Stage) titleLabel.getScene().getWindow()); + } + + private void copyOwnId() + { + Mono certificate = locationClient.getRSId(OWN_LOCATION_ID); + certificate.subscribe(reply -> Platform.runLater(() -> { + var clipboard = Clipboard.getSystemClipboard(); + var content = new ClipboardContent(); + content.putString(reply.RSId()); + clipboard.setContent(content); + })); + } + + private void addFriend() + { + windowManager.openAddFriend(titleLabel.getScene().getWindow()); + } + + private void launchBrowser() + { + JavaFxApplication.openUrl(JavaFxApplication.getControlUrl()); + } +} diff --git a/ui/src/main/java/io/xeres/ui/controller/WindowController.java b/ui/src/main/java/io/xeres/ui/controller/WindowController.java new file mode 100644 index 000000000..2ead37f4e --- /dev/null +++ b/ui/src/main/java/io/xeres/ui/controller/WindowController.java @@ -0,0 +1,51 @@ +/* + * Copyright (c) 2019-2021 by David Gerber - https://zapek.com + * + * This file is part of Xeres. + * + * Xeres is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Xeres is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Xeres. If not, see . + */ + +package io.xeres.ui.controller; + +/** + * Use this interface when building a Window. + */ +public interface WindowController extends Controller +{ + default void onShowing() + { + // default + } + + default void onShown() + { + // default + } + + default void onCloseRequest() + { + // default + } + + default void onHiding() + { + // default + } + + default void onHidden() + { + // default + } +} diff --git a/ui/src/main/java/io/xeres/ui/controller/about/AboutWindowController.java b/ui/src/main/java/io/xeres/ui/controller/about/AboutWindowController.java new file mode 100644 index 000000000..e796609e9 --- /dev/null +++ b/ui/src/main/java/io/xeres/ui/controller/about/AboutWindowController.java @@ -0,0 +1,56 @@ +/* + * Copyright (c) 2019-2021 by David Gerber - https://zapek.com + * + * This file is part of Xeres. + * + * Xeres is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Xeres is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Xeres. If not, see . + */ + +package io.xeres.ui.controller.about; + +import io.xeres.ui.JavaFxApplication; +import io.xeres.ui.controller.WindowController; +import io.xeres.ui.support.util.UiUtils; +import javafx.application.Platform; +import javafx.fxml.FXML; +import javafx.scene.control.Button; +import javafx.scene.control.Hyperlink; +import javafx.scene.control.TextArea; +import net.rgielen.fxweaver.core.FxmlView; +import org.springframework.stereotype.Component; + +import java.io.IOException; + +@Component +@FxmlView(value = "/view/about/about.fxml") +public class AboutWindowController implements WindowController +{ + @FXML + private Button closeWindow; + + @FXML + private Hyperlink website; + + @FXML + private TextArea licenseTextArea; + + public void initialize() throws IOException + { + licenseTextArea.setText(UiUtils.getResourceFileAsString(getClass().getResourceAsStream("/LICENSE"))); + + closeWindow.setOnAction(UiUtils::closeWindow); + website.setOnAction(event -> JavaFxApplication.openUrl("https://xeres.io/")); + Platform.runLater(() -> closeWindow.requestFocus()); + } +} diff --git a/ui/src/main/java/io/xeres/ui/controller/account/AccountCreationWindowController.java b/ui/src/main/java/io/xeres/ui/controller/account/AccountCreationWindowController.java new file mode 100644 index 000000000..88036e6f7 --- /dev/null +++ b/ui/src/main/java/io/xeres/ui/controller/account/AccountCreationWindowController.java @@ -0,0 +1,177 @@ +/* + * Copyright (c) 2019-2021 by David Gerber - https://zapek.com + * + * This file is part of Xeres. + * + * Xeres is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Xeres is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Xeres. If not, see . + */ + +package io.xeres.ui.controller.account; + +import io.xeres.ui.client.ConfigClient; +import io.xeres.ui.controller.WindowController; +import io.xeres.ui.support.util.UiUtils; +import io.xeres.ui.support.window.WindowManager; +import javafx.application.Platform; +import javafx.fxml.FXML; +import javafx.scene.control.Button; +import javafx.scene.control.Label; +import javafx.scene.control.ProgressIndicator; +import javafx.scene.control.TextField; +import net.rgielen.fxweaver.core.FxmlView; +import org.springframework.stereotype.Component; +import reactor.core.publisher.Mono; + +import static org.apache.commons.lang3.StringUtils.isNotBlank; + +@Component +@FxmlView(value = "/view/account/account_creation.fxml") +public class AccountCreationWindowController implements WindowController +{ + @FXML + private Button okButton; + + @FXML + private TextField profileName; + + @FXML + private TextField locationName; + + @FXML + private ProgressIndicator progress; + + @FXML + private Label status; + + private final ConfigClient configClient; + private final WindowManager windowManager; + + public AccountCreationWindowController(ConfigClient configClient, WindowManager windowManager) + { + this.configClient = configClient; + this.windowManager = windowManager; + } + + @Override + public void initialize() + { + profileName.textProperty().addListener(observable -> okButton.setDisable(profileName.getText().isBlank())); + locationName.textProperty().addListener(observable -> okButton.setDisable(locationName.getText().isBlank())); + + configClient.getUsername() + .doOnSuccess(usernameResult -> Platform.runLater(() -> profileName.setText(usernameResult.username()))) + .subscribe(); + + configClient.getHostname() + .doOnSuccess(hostnameResult -> Platform.runLater(() -> locationName.setText(sanitizeHostname(hostnameResult.hostname())))) + .subscribe(); + + okButton.setOnAction(actionEvent -> + { + String profileNameText = profileName.getText(); + String locationNameText = locationName.getText(); + if (isNotBlank(profileNameText) && isNotBlank(locationNameText)) + { + generateProfileAndLocation(profileNameText, locationNameText); + } + }); + } + + /** + * Try to make the hostname better by removing the domain part, if present. + * ie. bar.foo.baz -> bar + * + * @param hostname a hostname + * @return a hostname without the domain part + */ + private String sanitizeHostname(String hostname) + { + return hostname.split("\\.")[0]; + } + + private void setInProgress(boolean inProgress) + { + if (inProgress) + { + okButton.setDisable(true); + profileName.setDisable(true); + locationName.setDisable(true); + progress.setVisible(true); + } + else + { + okButton.setDisable(false); + profileName.setDisable(false); + locationName.setDisable(false); + progress.setVisible(false); + } + } + + public void generateProfileAndLocation(String profileName, String locationName) + { + setInProgress(true); + + Mono result = configClient.createProfile(profileName); + + status.setText("Generating profile keys..."); + + result.doOnSuccess(aVoid -> Platform.runLater(() -> generateLocation(profileName, locationName))) + .doOnError(throwable -> Platform.runLater(() -> { + UiUtils.showError(this.profileName, "Error while creating profile"); + setInProgress(false); + })) + .subscribe(); + } + + private void generateLocation(String profileName, String locationName) + { + setInProgress(true); + + Mono result = configClient.createLocation(locationName); + + status.setText("Generating location keys and certificate..."); + + result.doOnSuccess(aVoid -> Platform.runLater(() -> generateIdentity(profileName))) + .doOnError(throwable -> Platform.runLater(() -> + { + UiUtils.showAlertError("Location creation", "Unexpected error", "..."); + setInProgress(false); + })) + .subscribe(); + } + + private void generateIdentity(String identityName) + { + setInProgress(true); + + Mono result = configClient.createIdentity(identityName, false); + + status.setText("Generating identity..."); + + result.doOnSuccess(identityResponse -> Platform.runLater(this::openDashboard)) + .doOnError(throwable -> Platform.runLater(() -> + { + UiUtils.showAlertError("Identity creation", "Unexpected error", "..."); + setInProgress(false); + })) + .subscribe(); + } + + public void openDashboard() + { + windowManager.openMain(null); + + profileName.getScene().getWindow().hide(); + } +} diff --git a/ui/src/main/java/io/xeres/ui/controller/chat/ChatListView.java b/ui/src/main/java/io/xeres/ui/controller/chat/ChatListView.java new file mode 100644 index 000000000..81f44dd56 --- /dev/null +++ b/ui/src/main/java/io/xeres/ui/controller/chat/ChatListView.java @@ -0,0 +1,81 @@ +/* + * Copyright (c) 2019-2021 by David Gerber - https://zapek.com + * + * This file is part of Xeres. + * + * Xeres is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Xeres is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Xeres. If not, see . + */ + +package io.xeres.ui.controller.chat; + +import io.xeres.common.message.chat.RoomInfo; +import io.xeres.ui.custom.ChatListCell; +import io.xeres.ui.custom.NullSelectionModel; +import javafx.collections.FXCollections; +import javafx.collections.ObservableList; +import javafx.scene.control.ListView; +import javafx.scene.layout.Priority; +import javafx.scene.layout.VBox; + +// XXX: custom object which contains a ListView and its messages... +public class ChatListView +{ + private final ObservableList messages = FXCollections.observableArrayList(); + + private String nickname; + private final RoomInfo roomInfo; + + private final ListView listView; + + public ChatListView(String nickname, RoomInfo roomInfo) + { + this.nickname = nickname; + this.roomInfo = roomInfo; + + listView = new ListView<>(); + listView.setFocusTraversable(false); + listView.getStyleClass().add("chatlist"); + VBox.setVgrow(listView, Priority.ALWAYS); + + listView.setCellFactory(ChatListCell::new); + listView.setItems(messages); + listView.setSelectionModel(new NullSelectionModel()); + listView.setMouseTransparent(true); + } + + public void addMessage(String message) + { + messages.add("<" + nickname + "> " + message); + } + + public void addMessage(String from, String message) + { + messages.add("<" + from + "> " + message); + } + + public void setNickname(String nickname) + { + this.nickname = nickname; + } + + public ListView getListView() + { + return listView; + } + + public RoomInfo getRoomInfo() + { + return roomInfo; + } +} diff --git a/ui/src/main/java/io/xeres/ui/controller/chat/ChatRoomCreationWindowController.java b/ui/src/main/java/io/xeres/ui/controller/chat/ChatRoomCreationWindowController.java new file mode 100644 index 000000000..0c62d966a --- /dev/null +++ b/ui/src/main/java/io/xeres/ui/controller/chat/ChatRoomCreationWindowController.java @@ -0,0 +1,66 @@ +/* + * Copyright (c) 2019-2021 by David Gerber - https://zapek.com + * + * This file is part of Xeres. + * + * Xeres is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Xeres is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Xeres. If not, see . + */ + +package io.xeres.ui.controller.chat; + +import io.xeres.ui.client.ChatClient; +import io.xeres.ui.controller.WindowController; +import io.xeres.ui.support.util.UiUtils; +import javafx.application.Platform; +import javafx.fxml.FXML; +import javafx.scene.control.Button; +import javafx.scene.control.TextField; +import net.rgielen.fxweaver.core.FxmlView; +import org.springframework.stereotype.Component; + +@Component +@FxmlView(value = "/view/chat/chatroom_create.fxml") +public class ChatRoomCreationWindowController implements WindowController +{ + @FXML + private Button createButton; + + @FXML + private Button cancelButton; + + @FXML + private TextField roomName; + + @FXML + private TextField topic; + + private final ChatClient chatClient; + + public ChatRoomCreationWindowController(ChatClient chatClient) + { + this.chatClient = chatClient; + } + + @Override + public void initialize() + { + roomName.textProperty().addListener(observable -> createButton.setDisable(roomName.getText().isBlank())); + topic.textProperty().addListener(observable -> createButton.setDisable(topic.getText().isBlank())); + + createButton.setOnAction(event -> chatClient.createChatRoom(roomName.getText(), topic.getText()) + .doOnSuccess(aVoid -> Platform.runLater(() -> UiUtils.closeWindow(roomName))) + .subscribe()); + cancelButton.setOnAction(UiUtils::closeWindow); + } +} diff --git a/ui/src/main/java/io/xeres/ui/controller/chat/ChatRoomInfoController.java b/ui/src/main/java/io/xeres/ui/controller/chat/ChatRoomInfoController.java new file mode 100644 index 000000000..7abc96eda --- /dev/null +++ b/ui/src/main/java/io/xeres/ui/controller/chat/ChatRoomInfoController.java @@ -0,0 +1,66 @@ +/* + * Copyright (c) 2019-2021 by David Gerber - https://zapek.com + * + * This file is part of Xeres. + * + * Xeres is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Xeres is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Xeres. If not, see . + */ + +package io.xeres.ui.controller.chat; + +import io.xeres.common.id.Id; +import io.xeres.common.message.chat.RoomInfo; +import io.xeres.ui.controller.Controller; +import javafx.fxml.FXML; +import javafx.scene.control.Label; + +public class ChatRoomInfoController implements Controller +{ + @FXML + private Label roomName; + + @FXML + private Label roomId; + + @FXML + private Label roomTopic; + + @FXML + private Label roomType; + + @FXML + private Label roomSecurity; + + @FXML + private Label roomCount; + + @Override + public void initialize() + { + // Nothing to do + } + + public void setRoomInfo(RoomInfo roomInfo) + { + this.roomName.setText(roomInfo.getName()); + + this.roomId.setText(roomInfo.getId() != 0L ? Id.toString(roomInfo.getId()) : ""); + + this.roomTopic.setText(roomInfo.getTopic() != null ? roomInfo.getTopic() : ""); + + this.roomType.setText(roomInfo.getRoomType() != null ? roomInfo.getRoomType().toString() : ""); + this.roomSecurity.setText(roomInfo.isSigned() ? "Signed IDs" : "All IDs"); + this.roomCount.setText(String.valueOf(roomInfo.getCount())); + } +} diff --git a/ui/src/main/java/io/xeres/ui/controller/chat/ChatViewController.java b/ui/src/main/java/io/xeres/ui/controller/chat/ChatViewController.java new file mode 100644 index 000000000..daba038fd --- /dev/null +++ b/ui/src/main/java/io/xeres/ui/controller/chat/ChatViewController.java @@ -0,0 +1,319 @@ +/* + * Copyright (c) 2019-2021 by David Gerber - https://zapek.com + * + * This file is part of Xeres. + * + * Xeres is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Xeres is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Xeres. If not, see . + */ + +package io.xeres.ui.controller.chat; + +import io.xeres.common.message.chat.ChatMessage; +import io.xeres.common.message.chat.ChatRoomMessage; +import io.xeres.common.message.chat.RoomInfo; +import io.xeres.common.message.chat.RoomType; +import io.xeres.ui.client.ChatClient; +import io.xeres.ui.client.ProfileClient; +import io.xeres.ui.client.message.MessageClient; +import io.xeres.ui.controller.Controller; +import javafx.application.Platform; +import javafx.collections.ObservableList; +import javafx.fxml.FXML; +import javafx.fxml.FXMLLoader; +import javafx.scene.Node; +import javafx.scene.control.*; +import javafx.scene.input.KeyCode; +import javafx.scene.layout.Priority; +import javafx.scene.layout.VBox; +import net.rgielen.fxweaver.core.FxmlView; +import org.springframework.stereotype.Component; + +import java.io.IOException; +import java.time.Duration; +import java.time.Instant; +import java.util.Comparator; +import java.util.List; +import java.util.Optional; + +import static io.xeres.common.message.chat.ChatConstants.TYPING_NOTIFICATION_DELAY; +import static org.apache.commons.lang3.StringUtils.isNotBlank; + +@Component +@FxmlView(value = "/view/chat/chatview.fxml") +public class ChatViewController implements Controller +{ + @FXML + private TreeView roomTree; + + @FXML + private VBox content; + + @FXML + private TextField send; + + private final MessageClient messageClient; + private final ChatClient chatClient; + private final ProfileClient profileClient; + + private final TreeItem subscribedRooms = new TreeItem<>(new RoomHolder("Subscribed")); + private final TreeItem privateRooms = new TreeItem<>(new RoomHolder("Private")); + private final TreeItem publicRooms = new TreeItem<>(new RoomHolder("Public")); + + private String nickname; + + private RoomInfo selectedRoom; + private ChatListView selectedChatListView; + private Node roomInfoView; + private ChatRoomInfoController chatRoomInfoController; + + private Instant lastTypingNotification = Instant.EPOCH; + + public ChatViewController(MessageClient messageClient, ChatClient chatClient, ProfileClient profileClient) + { + this.messageClient = messageClient; + this.chatClient = chatClient; + this.profileClient = profileClient; + } + + public void initialize() throws IOException + { + profileClient.getOwnProfile().doOnSuccess(profile -> nickname = profile.getName()) + .subscribe(); + + TreeItem root = new TreeItem<>(new RoomHolder()); + //noinspection unchecked + root.getChildren().addAll(subscribedRooms, privateRooms, publicRooms); + root.setExpanded(true); + roomTree.setRoot(root); + roomTree.setShowRoot(false); + roomTree.setCellFactory(param -> { + TreeCell cell = new TreeCell<>() + { + @Override + protected void updateItem(RoomHolder roomHolder, boolean empty) + { + super.updateItem(roomHolder, empty); + if (empty) + { + setText(null); + } + else + { + setText(roomHolder.getRoomInfo().getName()); + } + } + }; + var cm = createRoomContextMenu(cell); + cell.setContextMenu(cm); + return cell; + }); + + // We need Platform.runLater() because when an entry is moved, the selection can change + roomTree.getSelectionModel().selectedItemProperty() + .addListener((observable, oldValue, newValue) -> Platform.runLater(() -> changeSelectedRoom(newValue.getValue().getRoomInfo()))); + + roomTree.setOnMouseClicked(event -> { + if (event.getClickCount() == 2 && selectedRoom != null && selectedRoom.getId() != 0) + { + chatClient.joinChatRoom(selectedRoom.getId()) + .subscribe(); // XXX: only do that if we're not already subscribed + } + }); + + send.setOnKeyPressed(event -> + { + if (selectedRoom != null && selectedRoom.getId() != 0) + { + if (event.getCode().equals(KeyCode.ENTER) && isNotBlank(send.getText())) + { + var chatMessage = new ChatMessage(send.getText()); + messageClient.sendToChatRoom(selectedRoom.getId(), chatMessage); + selectedChatListView.addMessage(send.getText()); + send.clear(); + } + else + { + var now = Instant.now(); + if (Duration.between(lastTypingNotification, now).compareTo(TYPING_NOTIFICATION_DELAY) > 0) + { + var chatMessage = new ChatMessage(); + messageClient.sendToChatRoom(selectedRoom.getId(), chatMessage); + lastTypingNotification = now; + } + } + } + }); + + var loader = new FXMLLoader(getClass().getResource("/view/chat/chat_roominfo.fxml")); + roomInfoView = loader.load(); + chatRoomInfoController = loader.getController(); + + VBox.setVgrow(roomInfoView, Priority.ALWAYS); + switchChatContent(roomInfoView); + send.setVisible(false); + } + + private ContextMenu createRoomContextMenu(TreeCell cell) + { + // XXX: isn't there a better way to find which object the context menu is called upon? + var cm = new ContextMenu(); + + var subscribeItem = new MenuItem("Join"); + subscribeItem.setOnAction(event -> { + var roomInfo = cell.getTreeItem().getValue().getRoomInfo(); + boolean found = subscribedRooms.getChildren().stream() + .noneMatch(roomHolderTreeItem -> roomHolderTreeItem.getValue().getRoomInfo().equals(roomInfo)); + + if (!found) + { + chatClient.joinChatRoom(selectedRoom.getId()) + .subscribe(); // XXX: only do that if we're not already subscribed. also doOnSuccess(), etc... sometimes the id of the roomInfo is wrong... of course! because we set the context menu BEFORE the room id is refreshed into it + } + }); + + var unsubscribeItem = new MenuItem("Leave"); + unsubscribeItem.setOnAction(event -> { + var roomInfo = cell.getTreeItem().getValue().getRoomInfo(); + subscribedRooms.getChildren().stream() + .filter(roomHolderTreeItem -> roomHolderTreeItem.getValue().getRoomInfo().equals(roomInfo)) + .findAny() + .ifPresent(roomHolderTreeItem -> chatClient.leaveChatRoom(roomInfo.getId()) + .subscribe()); + }); + cm.getItems().addAll(subscribeItem, unsubscribeItem); + return cm; + } + + public void addRooms(List newRooms) + { + ObservableList> subscribedTree = subscribedRooms.getChildren(); + ObservableList> publicTree = publicRooms.getChildren(); + ObservableList> privateTree = privateRooms.getChildren(); + + // Make sure we don't add rooms that we're already subscribed to + List unsubscribedRooms = newRooms.stream() + .filter(roomInfo -> !isInside(subscribedTree, roomInfo)) + .toList(); + + unsubscribedRooms.stream() + .filter(roomInfo -> roomInfo.getRoomType() == RoomType.PUBLIC) + .sorted(Comparator.comparing(RoomInfo::getName)) + .forEach(roomInfo -> addOrUpdate(publicTree, roomInfo)); + + unsubscribedRooms.stream() + .filter(roomInfo -> roomInfo.getRoomType() == RoomType.PRIVATE) + .sorted(Comparator.comparing(RoomInfo::getName)) + .forEach(roomInfo -> addOrUpdate(privateTree, roomInfo)); + } + + public void roomJoined(long roomId) + { + // Must be idempotent + publicRooms.getChildren().stream() + .filter(roomInfoTreeItem -> roomInfoTreeItem.getValue().getRoomInfo().getId() == roomId) + .findFirst() + .ifPresent(roomHolderTreeItem -> { + publicRooms.getChildren().remove(roomHolderTreeItem); + subscribedRooms.getChildren().add(roomHolderTreeItem); + }); + // XXX: also do it for private + } + + public void roomLeft(long roomId) + { + // Must be idempotent + subscribedRooms.getChildren().stream() + .filter(roomInfoTreeItem -> roomInfoTreeItem.getValue().getRoomInfo().getId() == roomId) + .findFirst() + .ifPresent(roomHolderTreeItem -> { + subscribedRooms.getChildren().remove(roomHolderTreeItem); + publicRooms.getChildren().add(roomHolderTreeItem); // XXX: could be public too... + roomHolderTreeItem.getValue().clearChatListView(); + }); + } + + private void switchChatContent(Node node) + { + if (content.getChildren().size() > 1) + { + content.getChildren().remove(0); + } + content.getChildren().add(0, node); + } + + // XXX: also we should merge/refresh... (ie. new rooms added, older rooms removed, etc...). merging properly is very difficult it seems + // right now I use a simple implementation. It also has a drawback that it doesn't sort new entries and doesn't update the counter + private void addOrUpdate(ObservableList> tree, RoomInfo roomInfo) + { + if (tree.stream() + .map(TreeItem::getValue) + .noneMatch(existingRoom -> existingRoom.getRoomInfo().equals(roomInfo))) + { + tree.add(new TreeItem<>(new RoomHolder(roomInfo))); + } + } + + private boolean isInside(ObservableList> tree, RoomInfo roomInfo) + { + return tree.stream() + .map(TreeItem::getValue) + .anyMatch(roomHolder -> roomHolder.getRoomInfo().equals(roomInfo)); + } + + private void changeSelectedRoom(RoomInfo roomInfo) + { + selectedRoom = roomInfo; + + Optional> treeItem = subscribedRooms.getChildren().stream() + .filter(roomInfoTreeItem -> roomInfoTreeItem.getValue().getRoomInfo().equals(roomInfo)) + .findFirst(); + + if (treeItem.isPresent()) + { + TreeItem roomInfoTreeItem = treeItem.get(); + var chatListView = roomInfoTreeItem.getValue().getChatListView(); + if (chatListView == null) + { + chatListView = new ChatListView(nickname, roomInfo); + roomInfoTreeItem.getValue().setChatListView(chatListView); + } + selectedChatListView = chatListView; + switchChatContent(chatListView.getListView()); + send.setVisible(true); + } + else + { + chatRoomInfoController.setRoomInfo(roomInfo); + switchChatContent(roomInfoView); + send.setVisible(false); + selectedChatListView = null; + } + } + + public void showMessage(ChatRoomMessage chatRoomMessage) + { + if (chatRoomMessage.isEmpty()) + { + // XXX: show a typing notification somewhere + } + else + { + subscribedRooms.getChildren().stream() + .map(roomInfoTreeItem -> roomInfoTreeItem.getValue().getChatListView()) + .filter(chatListView -> chatListView.getRoomInfo().getId() == chatRoomMessage.getRoomId()) + .findFirst() + .ifPresent(chatListView -> chatListView.addMessage(chatRoomMessage.getSenderNickname(), chatRoomMessage.getContent())); + } + } +} diff --git a/ui/src/main/java/io/xeres/ui/controller/chat/RoomHolder.java b/ui/src/main/java/io/xeres/ui/controller/chat/RoomHolder.java new file mode 100644 index 000000000..f9485ce82 --- /dev/null +++ b/ui/src/main/java/io/xeres/ui/controller/chat/RoomHolder.java @@ -0,0 +1,75 @@ +/* + * Copyright (c) 2019-2021 by David Gerber - https://zapek.com + * + * This file is part of Xeres. + * + * Xeres is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Xeres is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Xeres. If not, see . + */ + +package io.xeres.ui.controller.chat; + +import io.xeres.common.message.chat.RoomInfo; + +public class RoomHolder +{ + private ChatListView chatListView; + private final RoomInfo roomInfo; + + public RoomHolder(ChatListView chatListView, RoomInfo roomInfo) + { + this.chatListView = chatListView; + this.roomInfo = roomInfo; + } + + public RoomHolder(String name) + { + this.roomInfo = new RoomInfo(name); + } + + public RoomHolder(RoomInfo roomInfo) + { + this.roomInfo = roomInfo; + } + + public RoomHolder() + { + this.roomInfo = new RoomInfo(""); + } + + public void setChatListView(ChatListView chatListView) + { + this.chatListView = chatListView; + } + + public void clearChatListView() + { + this.chatListView = null; + } + + public ChatListView getChatListView() + { + return chatListView; + } + + public RoomInfo getRoomInfo() + { + return roomInfo; + } + + @Override + public String toString() + { + return roomInfo.getName(); + } +} diff --git a/ui/src/main/java/io/xeres/ui/controller/id/AddCertificateWindowController.java b/ui/src/main/java/io/xeres/ui/controller/id/AddCertificateWindowController.java new file mode 100644 index 000000000..3a257392e --- /dev/null +++ b/ui/src/main/java/io/xeres/ui/controller/id/AddCertificateWindowController.java @@ -0,0 +1,153 @@ +/* + * Copyright (c) 2019-2021 by David Gerber - https://zapek.com + * + * This file is part of Xeres. + * + * Xeres is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Xeres is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Xeres. If not, see . + */ + +package io.xeres.ui.controller.id; + +import io.xeres.common.id.Id; +import io.xeres.ui.client.ProfileClient; +import io.xeres.ui.controller.WindowController; +import io.xeres.ui.model.connection.Connection; +import io.xeres.ui.support.util.UiUtils; +import javafx.application.Platform; +import javafx.fxml.FXML; +import javafx.scene.control.*; +import net.rgielen.fxweaver.core.FxmlView; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.stereotype.Component; +import reactor.core.publisher.Mono; + +import static java.util.function.Predicate.not; + +@Component +@FxmlView(value = "/view/id/certificate_add.fxml") +public class AddCertificateWindowController implements WindowController +{ + private static final Logger log = LoggerFactory.getLogger(AddCertificateWindowController.class); + + @FXML + private Button cancelButton; + + @FXML + private Button addButton; + + @FXML + private TextArea certificateTextArea; + + @FXML + private TextField certName; + + @FXML + private TextField certId; + + @FXML + private TextField certFingerprint; + + @FXML + private TextField certLocName; + + @FXML + private TextField certLocId; + + @FXML + private TextField certIp; + + @FXML + private TitledPane titledPane; + + private final ProfileClient profileClient; + + public AddCertificateWindowController(ProfileClient profileClient) + { + this.profileClient = profileClient; + } + + public void initialize() + { + addButton.setOnAction(event -> addFriend()); + cancelButton.setOnAction(UiUtils::closeWindow); + certificateTextArea.textProperty().addListener((observable, oldValue, newValue) -> checkCertificate(newValue)); // XXX: add a debouncer for this + } + + private void addFriend() + { + Mono profile = profileClient.createProfile(certificateTextArea.getText()); + + profile.doOnSuccess(aVoid -> Platform.runLater(() -> UiUtils.closeWindow(cancelButton))) + .doOnError(throwable -> log.error("Error: {}", throwable.getMessage())) + .subscribe(); + } + + private void checkCertificate(String certificateString) + { + profileClient.checkCertificate(certificateString.replaceAll("([\r\n\t])", "")) + .doOnSuccess(profile -> Platform.runLater(() -> + { + if (profile.getPgpPublicKeyData() != null) + { + certificateTextArea.setTooltip(new Tooltip("A RS Certificate cannot be used. Please use a Short ID instead (version higher than Retroshare 0.6.5)")); + addButton.setDisable(true); + UiUtils.showWarning(certificateTextArea); + } + else + { + certificateTextArea.setTooltip(new Tooltip("ID is valid")); + addButton.setDisable(false); + UiUtils.clearError(certificateTextArea); + } + + certName.setText(profile.getName()); + certId.setText(Id.toString(profile.getPgpIdentifier())); + certFingerprint.setText(profile.getProfileFingerprint().toString()); + profile.getLocations().stream() + .findFirst() + .ifPresent(location -> + { + // XXX: display the hostname if available! + certLocName.setText(location.getName()); + certLocId.setText(location.getLocationId().toString()); + location.getConnections().stream() + .filter(Connection::isExternal) + .findFirst().ifPresent(connection -> + certIp.setText(connection.getAddress())); + + location.getConnections().stream() + .filter(not(Connection::isExternal)) + .findFirst().ifPresent(connection -> + certIp.setTooltip(new Tooltip("LAN address: " + connection.getAddress()))); + }); + titledPane.setExpanded(true); + })) + .doOnError(throwable -> + { + certificateTextArea.setTooltip(new Tooltip(throwable.getMessage())); + addButton.setDisable(true); + if (certificateTextArea.getText().isBlank()) + { + UiUtils.clearError(certificateTextArea); + } + else + { + UiUtils.showError(certificateTextArea); + } + titledPane.setExpanded(false); + }) + .subscribe(); + } +} diff --git a/ui/src/main/java/io/xeres/ui/controller/messaging/BroadcastWindowController.java b/ui/src/main/java/io/xeres/ui/controller/messaging/BroadcastWindowController.java new file mode 100644 index 000000000..4bdd1adb4 --- /dev/null +++ b/ui/src/main/java/io/xeres/ui/controller/messaging/BroadcastWindowController.java @@ -0,0 +1,65 @@ +/* + * Copyright (c) 2019-2021 by David Gerber - https://zapek.com + * + * This file is part of Xeres. + * + * Xeres is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Xeres is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Xeres. If not, see . + */ + +package io.xeres.ui.controller.messaging; + +import io.xeres.common.message.chat.ChatMessage; +import io.xeres.ui.client.message.MessageClient; +import io.xeres.ui.controller.WindowController; +import io.xeres.ui.support.util.UiUtils; +import javafx.fxml.FXML; +import javafx.scene.control.Button; +import javafx.scene.control.TextArea; +import net.rgielen.fxweaver.core.FxmlView; +import org.springframework.stereotype.Component; + +@Component +@FxmlView(value = "/view/messaging/broadcast.fxml") +public class BroadcastWindowController implements WindowController +{ + @FXML + private Button send; + + @FXML + private Button cancel; + + @FXML + private TextArea textArea; + + private final MessageClient messageClient; + + public BroadcastWindowController(MessageClient messageClient) + { + this.messageClient = messageClient; + } + + @Override + public void initialize() + { + send.setOnAction(event -> + { + var message = new ChatMessage(textArea.getText()); + messageClient.sendBroadcast(message); + cancel.fire(); + }); + + textArea.textProperty().addListener(observable -> send.setDisable(textArea.getText().isBlank())); + cancel.setOnAction(UiUtils::closeWindow); + } +} diff --git a/ui/src/main/java/io/xeres/ui/controller/messaging/FriendsWindowController.java b/ui/src/main/java/io/xeres/ui/controller/messaging/FriendsWindowController.java new file mode 100644 index 000000000..5ef1a1d0f --- /dev/null +++ b/ui/src/main/java/io/xeres/ui/controller/messaging/FriendsWindowController.java @@ -0,0 +1,123 @@ +/* + * Copyright (c) 2019-2021 by David Gerber - https://zapek.com + * + * This file is part of Xeres. + * + * Xeres is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Xeres is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Xeres. If not, see . + */ + +package io.xeres.ui.controller.messaging; + +import io.xeres.ui.client.ConnectionClient; +import io.xeres.ui.client.ProfileClient; +import io.xeres.ui.controller.WindowController; +import io.xeres.ui.support.window.WindowManager; +import javafx.application.Platform; +import javafx.fxml.FXML; +import javafx.scene.control.Label; +import javafx.scene.control.TreeCell; +import javafx.scene.control.TreeItem; +import javafx.scene.control.TreeView; +import net.rgielen.fxweaver.core.FxmlView; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.stereotype.Component; + +@Component +@FxmlView(value = "/view/messaging/friends.fxml") +public class FriendsWindowController implements WindowController +{ + private static final Logger log = LoggerFactory.getLogger(FriendsWindowController.class); + + @FXML + private Label nickname; + + @FXML + private TreeView friendsTree; + + private final ProfileClient profileClient; + private final ConnectionClient connectionClient; + private final WindowManager windowManager; + + public FriendsWindowController(ProfileClient profileClient, ConnectionClient connectionClient, WindowManager windowManager) + { + this.profileClient = profileClient; + this.connectionClient = connectionClient; + this.windowManager = windowManager; + } + + @Override + public void initialize() + { + TreeItem root = new TreeItem<>(new ProfileHolder()); + root.setExpanded(true); + friendsTree.setRoot(root); + friendsTree.setShowRoot(false); + + friendsTree.setCellFactory(param -> + { + TreeCell cell = new TreeCell<>() + { + @Override + protected void updateItem(ProfileHolder profileHolder, boolean empty) + { + super.updateItem(profileHolder, empty); + if (empty) + { + setText(null); + } + else + { + setText(profileHolder.getProfile().getName()); // XXX: add some logic for leaves, etc... + } + } + }; + // XXX: add context menu here, maybe + return cell; + }); + + friendsTree.setOnMouseClicked(event -> { + if (event.getClickCount() == 2) // XXX: add another condition to make sure we're double clicking on a leaf + { + var profileHolder = friendsTree.getSelectionModel().getSelectedItem().getValue(); + if (profileHolder.hasLocation()) + { + windowManager.openMessaging(profileHolder.getLocation().getLocationId().toString(), null); + } + } + }); + + profileClient.getOwnProfile() + .doOnSuccess(profile -> Platform.runLater(() -> nickname.setText(profile.getName()))) + .subscribe(); + + connectionClient.getConnectedProfiles().collectList() + .doOnSuccess(profiles -> Platform.runLater(() -> profiles.forEach(profile -> { + if (profile.getLocations().size() == 1) + { + root.getChildren().add(new TreeItem<>(new ProfileHolder(profile, profile.getLocations().get(0)))); + } + else + { + var parent = new TreeItem<>(new ProfileHolder(profile)); + root.getChildren().add(parent); + profile.getLocations().forEach(location -> parent.getChildren().add(new TreeItem<>(new ProfileHolder(profile, location)))); + } + }))) + .doOnError(throwable -> log.error("Error while getting profiles: {}", throwable.getMessage(), throwable)) + .subscribe(); + + // XXX: here lies a good example of a connection that should stay open to get refreshed... ponder how to do it + } +} diff --git a/ui/src/main/java/io/xeres/ui/controller/messaging/MessagingWindowController.java b/ui/src/main/java/io/xeres/ui/controller/messaging/MessagingWindowController.java new file mode 100644 index 000000000..17f7d7d1a --- /dev/null +++ b/ui/src/main/java/io/xeres/ui/controller/messaging/MessagingWindowController.java @@ -0,0 +1,161 @@ +/* + * Copyright (c) 2019-2021 by David Gerber - https://zapek.com + * + * This file is part of Xeres. + * + * Xeres is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Xeres is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Xeres. If not, see . + */ + +package io.xeres.ui.controller.messaging; + +import io.xeres.common.id.LocationId; +import io.xeres.common.message.chat.ChatMessage; +import io.xeres.ui.client.ProfileClient; +import io.xeres.ui.client.message.MessageClient; +import io.xeres.ui.controller.WindowController; +import io.xeres.ui.custom.ChatListCell; +import io.xeres.ui.custom.NullSelectionModel; +import io.xeres.ui.model.profile.Profile; +import javafx.animation.KeyFrame; +import javafx.animation.Timeline; +import javafx.application.Platform; +import javafx.collections.FXCollections; +import javafx.collections.ObservableList; +import javafx.fxml.FXML; +import javafx.scene.control.Label; +import javafx.scene.control.ListView; +import javafx.scene.control.TextField; +import javafx.scene.input.KeyCode; +import javafx.stage.Stage; +import javafx.util.Duration; +import net.rgielen.fxweaver.core.FxmlView; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import reactor.core.publisher.Mono; + +import java.time.Instant; + +import static io.xeres.common.message.chat.ChatConstants.TYPING_NOTIFICATION_DELAY; +import static org.apache.commons.lang3.StringUtils.isNotBlank; + +@FxmlView(value = "/view/messaging/messaging.fxml") +public class MessagingWindowController implements WindowController +{ + private static final Logger log = LoggerFactory.getLogger(MessagingWindowController.class); + + @FXML + private TextField send; + + @FXML + private Label notification; + + @FXML + private ListView receive; + + private final ProfileClient profileClient; + private final LocationId locationId; + private Profile targetProfile; + + private final ObservableList messages = FXCollections.observableArrayList(); + + private final MessageClient messageClient; + + private String nickname; + + private Instant lastTypingNotification = Instant.EPOCH; + + private Timeline lastTypingTimeline; + + public MessagingWindowController(ProfileClient profileClient, MessageClient messageClient, String locationId) + { + this.profileClient = profileClient; + this.messageClient = messageClient; + this.locationId = new LocationId(locationId); + } + + public void initialize() + { + Mono ownProfileResult = profileClient.getOwnProfile(); + ownProfileResult.doOnSuccess(profile -> nickname = profile.getName()) + .subscribe(); + + receive.setCellFactory(ChatListCell::new); + receive.setItems(messages); + receive.setSelectionModel(new NullSelectionModel()); + receive.setMouseTransparent(true); + + send.setOnKeyPressed(event -> + { + if (event.getCode().equals(KeyCode.ENTER) && isNotBlank(send.getText())) + { + var message = new ChatMessage(send.getText()); + messageClient.sendToLocation(locationId, message); + messages.add("<" + nickname + "> " + send.getText()); + send.clear(); + } + else + { + var now = Instant.now(); + if (java.time.Duration.between(lastTypingNotification, now).compareTo(TYPING_NOTIFICATION_DELAY) > 0) + { + var message = new ChatMessage(); + messageClient.sendToLocation(locationId, message); + lastTypingNotification = now; + } + } + }); + + lastTypingTimeline = new Timeline(new KeyFrame(Duration.seconds(5), + ae -> notification.setText(""))); + } + + @Override + public void onShown() + { + profileClient.findByLocationId(locationId).collectList() + .doOnSuccess(profiles -> { + targetProfile = profiles.stream().findFirst().orElseThrow(); + var stage = (Stage) send.getScene().getWindow(); + Platform.runLater(() -> + { + stage.setTitle(targetProfile.getName()); // XXX: add the location name? yes but we need to retrieve the location then + var chatMessage = (ChatMessage) send.getScene().getRoot().getUserData(); + if (chatMessage != null) + { + showMessage(chatMessage); + send.getScene().getRoot().setUserData(null); + } + }); + }) + .doOnError(throwable -> log.error("Error while getting the profiles: {}", throwable.getMessage(), throwable)) + .subscribe(); + } + + public void showMessage(ChatMessage message) + { + if (message != null) + { + if (message.isEmpty()) + { + notification.setText(targetProfile.getName() + " is typing..."); + lastTypingTimeline.playFromStart(); + } + else + { + messages.add("<" + targetProfile.getName() + "> " + message.getContent()); + notification.setText(""); + } + } + } +} diff --git a/ui/src/main/java/io/xeres/ui/controller/messaging/ProfileHolder.java b/ui/src/main/java/io/xeres/ui/controller/messaging/ProfileHolder.java new file mode 100644 index 000000000..1fbdab468 --- /dev/null +++ b/ui/src/main/java/io/xeres/ui/controller/messaging/ProfileHolder.java @@ -0,0 +1,65 @@ +/* + * Copyright (c) 2019-2021 by David Gerber - https://zapek.com + * + * This file is part of Xeres. + * + * Xeres is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Xeres is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Xeres. If not, see . + */ + +package io.xeres.ui.controller.messaging; + +import io.xeres.ui.model.location.Location; +import io.xeres.ui.model.profile.Profile; + +public class ProfileHolder // XXX: rename it as PeerHolder perhaps... +{ + private Profile profile; + private Location location; + + public ProfileHolder() + { + + } + + public ProfileHolder(Profile profile) + { + this.profile = profile; + } + + public ProfileHolder(Profile profile, Location location) + { + this.profile = profile; + this.location = location; + } + + public Profile getProfile() + { + return profile; + } + + public Location getLocation() + { + return location; + } + + public void setLocation(Location location) + { + this.location = location; + } + + public boolean hasLocation() + { + return location != null; + } +} diff --git a/ui/src/main/java/io/xeres/ui/controller/profile/ProfilesUiController.java b/ui/src/main/java/io/xeres/ui/controller/profile/ProfilesUiController.java new file mode 100644 index 000000000..e511c685e --- /dev/null +++ b/ui/src/main/java/io/xeres/ui/controller/profile/ProfilesUiController.java @@ -0,0 +1,78 @@ +/* + * Copyright (c) 2019-2021 by David Gerber - https://zapek.com + * + * This file is part of Xeres. + * + * Xeres is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Xeres is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Xeres. If not, see . + */ + +package io.xeres.ui.controller.profile; + +import io.xeres.common.id.Id; +import io.xeres.common.pgp.Trust; +import io.xeres.ui.client.ProfileClient; +import io.xeres.ui.controller.WindowController; +import io.xeres.ui.model.profile.Profile; +import javafx.application.Platform; +import javafx.beans.property.SimpleStringProperty; +import javafx.fxml.FXML; +import javafx.scene.control.TableColumn; +import javafx.scene.control.TableView; +import javafx.scene.control.cell.PropertyValueFactory; +import net.rgielen.fxweaver.core.FxmlView; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.stereotype.Component; + +@Component +@FxmlView(value = "/view/profile/profiles.fxml") +public class ProfilesUiController implements WindowController +{ + private static final Logger log = LoggerFactory.getLogger(ProfilesUiController.class); + + private final ProfileClient profileClient; + + @FXML + private TableView profilesTableView; + + @FXML + private TableColumn tableName; + + @FXML + private TableColumn tableIdentifier; + + @FXML + private TableColumn tableAccepted; + + @FXML + private TableColumn tableTrust; + + public ProfilesUiController(ProfileClient profileClient) + { + this.profileClient = profileClient; + } + + public void initialize() + { + tableName.setCellValueFactory(new PropertyValueFactory<>("name")); + tableIdentifier.setCellValueFactory(param -> new SimpleStringProperty(Id.toString(param.getValue().getPgpIdentifier()))); + tableAccepted.setCellValueFactory(new PropertyValueFactory<>("accepted")); + tableTrust.setCellValueFactory(new PropertyValueFactory<>("trust")); + + profileClient.getProfiles().collectList() + .doOnSuccess(profiles -> Platform.runLater(() -> profilesTableView.getItems().addAll(profiles))) + .doOnError(throwable -> log.error("Error while getting the profiles: {}", throwable.getMessage(), throwable)) + .subscribe(); + } +} diff --git a/ui/src/main/java/io/xeres/ui/custom/ChatListCell.java b/ui/src/main/java/io/xeres/ui/custom/ChatListCell.java new file mode 100644 index 000000000..daefdbd12 --- /dev/null +++ b/ui/src/main/java/io/xeres/ui/custom/ChatListCell.java @@ -0,0 +1,39 @@ +/* + * Copyright (c) 2019-2021 by David Gerber - https://zapek.com + * + * This file is part of Xeres. + * + * Xeres is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Xeres is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Xeres. If not, see . + */ + +package io.xeres.ui.custom; + +import javafx.scene.control.ListCell; +import javafx.scene.control.ListView; +import javafx.scene.text.FontSmoothingType; +import javafx.scene.text.Text; + +public class ChatListCell extends ListCell +{ + public ChatListCell(ListView listView) + { + Text text = new Text(); + text.setFontSmoothingType(FontSmoothingType.LCD); + text.wrappingWidthProperty().bind(listView.widthProperty().subtract(15)); + text.textProperty().bind(itemProperty()); + + setPrefWidth(0); + setGraphic(text); + } +} diff --git a/ui/src/main/java/io/xeres/ui/custom/NullSelectionModel.java b/ui/src/main/java/io/xeres/ui/custom/NullSelectionModel.java new file mode 100644 index 000000000..eadd7c6c8 --- /dev/null +++ b/ui/src/main/java/io/xeres/ui/custom/NullSelectionModel.java @@ -0,0 +1,120 @@ +/* + * Copyright (c) 2019-2021 by David Gerber - https://zapek.com + * + * This file is part of Xeres. + * + * Xeres is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Xeres is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Xeres. If not, see . + */ + +package io.xeres.ui.custom; + +import javafx.collections.FXCollections; +import javafx.collections.ObservableList; +import javafx.scene.control.MultipleSelectionModel; + +/** + * Allows to disable the selection in eg. ListViews. + */ +public class NullSelectionModel extends MultipleSelectionModel +{ + @Override + public ObservableList getSelectedIndices() + { + return FXCollections.emptyObservableList(); + } + + @Override + public ObservableList getSelectedItems() + { + return FXCollections.emptyObservableList(); + } + + @Override + public void selectIndices(int index, int... indices) + { + // Disabled + } + + @Override + public void selectAll() + { + // Disabled + } + + @Override + public void clearAndSelect(int index) + { + // Disabled + } + + @Override + public void select(int index) + { + // Disabled + } + + @Override + public void select(String obj) + { + // Disabled + } + + @Override + public void clearSelection(int index) + { + // Disabled + } + + @Override + public void clearSelection() + { + // Disabled + } + + @Override + public boolean isSelected(int index) + { + return false; + } + + @Override + public boolean isEmpty() + { + return false; + } + + @Override + public void selectPrevious() + { + // Disabled + } + + @Override + public void selectNext() + { + // Disabled + } + + @Override + public void selectFirst() + { + // Disabled + } + + @Override + public void selectLast() + { + // Disabled + } +} diff --git a/ui/src/main/java/io/xeres/ui/model/connection/Connection.java b/ui/src/main/java/io/xeres/ui/model/connection/Connection.java new file mode 100644 index 000000000..0c575d033 --- /dev/null +++ b/ui/src/main/java/io/xeres/ui/model/connection/Connection.java @@ -0,0 +1,70 @@ +/* + * Copyright (c) 2019-2021 by David Gerber - https://zapek.com + * + * This file is part of Xeres. + * + * Xeres is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Xeres is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Xeres. If not, see . + */ + +package io.xeres.ui.model.connection; + +import java.time.Instant; + +public class Connection +{ + private long id; + private String address; + private Instant lastConnected; + private boolean external; + + public long getId() + { + return id; + } + + public void setId(long id) + { + this.id = id; + } + + public String getAddress() + { + return address; + } + + public void setAddress(String address) + { + this.address = address; + } + + public Instant getLastConnected() + { + return lastConnected; + } + + public void setLastConnected(Instant lastConnected) + { + this.lastConnected = lastConnected; + } + + public boolean isExternal() + { + return external; + } + + public void setExternal(boolean external) + { + this.external = external; + } +} diff --git a/ui/src/main/java/io/xeres/ui/model/connection/ConnectionMapper.java b/ui/src/main/java/io/xeres/ui/model/connection/ConnectionMapper.java new file mode 100644 index 000000000..09465784b --- /dev/null +++ b/ui/src/main/java/io/xeres/ui/model/connection/ConnectionMapper.java @@ -0,0 +1,45 @@ +/* + * Copyright (c) 2019-2021 by David Gerber - https://zapek.com + * + * This file is part of Xeres. + * + * Xeres is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Xeres is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Xeres. If not, see . + */ + +package io.xeres.ui.model.connection; + +import io.xeres.common.dto.connection.ConnectionDTO; + +public final class ConnectionMapper +{ + private ConnectionMapper() + { + throw new UnsupportedOperationException("Utility class"); + } + + public static Connection fromDTO(ConnectionDTO dto) + { + if (dto == null) + { + return null; + } + + var connection = new Connection(); + connection.setId(dto.id()); + connection.setAddress(dto.address()); + connection.setExternal(dto.external()); + connection.setLastConnected(dto.lastConnected()); + return connection; + } +} diff --git a/ui/src/main/java/io/xeres/ui/model/location/Location.java b/ui/src/main/java/io/xeres/ui/model/location/Location.java new file mode 100644 index 000000000..e22085ba1 --- /dev/null +++ b/ui/src/main/java/io/xeres/ui/model/location/Location.java @@ -0,0 +1,103 @@ +/* + * Copyright (c) 2019-2021 by David Gerber - https://zapek.com + * + * This file is part of Xeres. + * + * Xeres is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Xeres is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Xeres. If not, see . + */ + +package io.xeres.ui.model.location; + +import io.xeres.common.id.LocationId; +import io.xeres.ui.model.connection.Connection; + +import java.time.Instant; +import java.util.ArrayList; +import java.util.List; + +public class Location +{ + private long id; + private String name; + private LocationId locationId; + private String hostname; + private final List connections = new ArrayList<>(); + private boolean connected; + private Instant lastConnected; + + public long getId() + { + return id; + } + + public void setId(long id) + { + this.id = id; + } + + public String getName() + { + return name; + } + + public void setName(String name) + { + this.name = name; + } + + public LocationId getLocationId() + { + return locationId; + } + + public void setLocationId(LocationId locationId) + { + this.locationId = locationId; + } + + public String getHostname() + { + return hostname; + } + + public void setHostname(String hostname) + { + this.hostname = hostname; + } + + public List getConnections() + { + return connections; + } + + public boolean isConnected() + { + return connected; + } + + public void setConnected(boolean connected) + { + this.connected = connected; + } + + public Instant getLastConnected() + { + return lastConnected; + } + + public void setLastConnected(Instant lastConnected) + { + this.lastConnected = lastConnected; + } +} diff --git a/ui/src/main/java/io/xeres/ui/model/location/LocationMapper.java b/ui/src/main/java/io/xeres/ui/model/location/LocationMapper.java new file mode 100644 index 000000000..3f3e1c5b8 --- /dev/null +++ b/ui/src/main/java/io/xeres/ui/model/location/LocationMapper.java @@ -0,0 +1,65 @@ +/* + * Copyright (c) 2019-2021 by David Gerber - https://zapek.com + * + * This file is part of Xeres. + * + * Xeres is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Xeres is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Xeres. If not, see . + */ + +package io.xeres.ui.model.location; + +import io.xeres.common.dto.location.LocationDTO; +import io.xeres.common.id.LocationId; +import io.xeres.ui.model.connection.ConnectionMapper; + +public final class LocationMapper +{ + private LocationMapper() + { + throw new UnsupportedOperationException("Utility class"); + } + + public static Location fromDTO(LocationDTO dto) + { + if (dto == null) + { + return null; + } + + var location = new Location(); + location.setId(dto.id()); + location.setName(dto.name()); + location.setLocationId(new LocationId(dto.locationIdentifier())); + location.setConnected(dto.connected()); + location.setLastConnected(dto.lastConnected()); + + return location; + } + + public static Location fromDeepDTO(LocationDTO dto) + { + if (dto == null) + { + return null; + } + + var location = fromDTO(dto); + + location.getConnections().addAll(dto.connections().stream() + .map(ConnectionMapper::fromDTO) + .toList()); + + return location; + } +} diff --git a/ui/src/main/java/io/xeres/ui/model/profile/Profile.java b/ui/src/main/java/io/xeres/ui/model/profile/Profile.java new file mode 100644 index 000000000..e5652c2a4 --- /dev/null +++ b/ui/src/main/java/io/xeres/ui/model/profile/Profile.java @@ -0,0 +1,114 @@ +/* + * Copyright (c) 2019-2021 by David Gerber - https://zapek.com + * + * This file is part of Xeres. + * + * Xeres is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Xeres is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Xeres. If not, see . + */ + +package io.xeres.ui.model.profile; + +import io.xeres.common.id.ProfileFingerprint; +import io.xeres.common.pgp.Trust; +import io.xeres.ui.model.location.Location; + +import java.util.ArrayList; +import java.util.List; + +public class Profile +{ + private long id; + private String name; + private long pgpIdentifier; + private ProfileFingerprint profileFingerprint; + private byte[] pgpPublicKeyData; + private boolean accepted; + private Trust trust; + private final List locations = new ArrayList<>(); + + public long getId() + { + return id; + } + + public void setId(long id) + { + this.id = id; + } + + public String getName() + { + return name; + } + + public void setName(String name) + { + this.name = name; + } + + public long getPgpIdentifier() + { + return pgpIdentifier; + } + + public void setPgpIdentifier(long pgpIdentifier) + { + this.pgpIdentifier = pgpIdentifier; + } + + public ProfileFingerprint getProfileFingerprint() + { + return profileFingerprint; + } + + public void setProfileFingerprint(ProfileFingerprint profileFingerprint) + { + this.profileFingerprint = profileFingerprint; + } + + public byte[] getPgpPublicKeyData() + { + return pgpPublicKeyData; + } + + public void setPgpPublicKeyData(byte[] pgpPublicKeyData) + { + this.pgpPublicKeyData = pgpPublicKeyData; + } + + public boolean isAccepted() + { + return accepted; + } + + public void setAccepted(boolean accepted) + { + this.accepted = accepted; + } + + public Trust getTrust() + { + return trust; + } + + public void setTrust(Trust trust) + { + this.trust = trust; + } + + public List getLocations() + { + return locations; + } +} diff --git a/ui/src/main/java/io/xeres/ui/model/profile/ProfileMapper.java b/ui/src/main/java/io/xeres/ui/model/profile/ProfileMapper.java new file mode 100644 index 000000000..2e48f13bb --- /dev/null +++ b/ui/src/main/java/io/xeres/ui/model/profile/ProfileMapper.java @@ -0,0 +1,66 @@ +/* + * Copyright (c) 2019-2021 by David Gerber - https://zapek.com + * + * This file is part of Xeres. + * + * Xeres is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Xeres is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Xeres. If not, see . + */ + +package io.xeres.ui.model.profile; + +import io.xeres.common.dto.profile.ProfileDTO; +import io.xeres.common.id.ProfileFingerprint; +import io.xeres.ui.model.location.LocationMapper; + +public final class ProfileMapper +{ + private ProfileMapper() + { + throw new UnsupportedOperationException("Utility class"); + } + + public static Profile fromDTO(ProfileDTO dto) + { + if (dto == null) + { + return null; + } + + var profile = new Profile(); + profile.setId(dto.id()); + profile.setName(dto.name()); + profile.setPgpIdentifier(Long.parseLong(dto.pgpIdentifier())); + profile.setProfileFingerprint(new ProfileFingerprint(dto.pgpFingerprint())); + profile.setPgpPublicKeyData(dto.pgpPublicKeyData()); + profile.setAccepted(dto.accepted()); + profile.setTrust(dto.trust()); + return profile; + } + + public static Profile fromDeepDTO(ProfileDTO dto) + { + if (dto == null) + { + return null; + } + + var profile = fromDTO(dto); + + profile.getLocations().addAll(dto.locations().stream() + .map(LocationMapper::fromDeepDTO) + .toList()); + + return profile; + } +} diff --git a/ui/src/main/java/io/xeres/ui/support/tray/TrayService.java b/ui/src/main/java/io/xeres/ui/support/tray/TrayService.java new file mode 100644 index 000000000..f7e9a550e --- /dev/null +++ b/ui/src/main/java/io/xeres/ui/support/tray/TrayService.java @@ -0,0 +1,159 @@ +/* + * Copyright (c) 2019-2021 by David Gerber - https://zapek.com + * + * This file is part of Xeres. + * + * Xeres is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Xeres is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Xeres. If not, see . + */ + +package io.xeres.ui.support.tray; + +import io.xeres.common.AppName; +import io.xeres.ui.support.window.WindowManager; +import javafx.application.Platform; +import javafx.stage.Stage; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.stereotype.Service; + +import javax.annotation.PreDestroy; +import java.awt.*; +import java.awt.event.MouseAdapter; +import java.awt.event.MouseEvent; + +import static org.apache.commons.lang3.StringUtils.isNotBlank; + +@Service +public class TrayService +{ + private static final Logger log = LoggerFactory.getLogger(TrayService.class); + + private static SystemTray systemTray; + private static TrayIcon trayIcon; + private static boolean hasSystemTray; + + private final WindowManager windowManager; + + public TrayService(WindowManager windowManager) + { + this.windowManager = windowManager; + } + + public void addSystemTray(Stage stage) + { + if (hasSystemTray) + { + return; + } + System.setProperty("java.awt.headless", "false"); + + if (!SystemTray.isSupported()) + { + log.error("System tray not supported"); + return; + } + + // Do not exit the platform when all windows are closed. + Platform.setImplicitExit(false); + + var exitItem = new MenuItem("Exit"); + exitItem.addActionListener(e -> + { + windowManager.closeAllWindows(); + Platform.exit(); + }); + + var friendsItem = new MenuItem("Friends"); + friendsItem.addActionListener(e -> + windowManager.openFriends()); + + var popupMenu = new PopupMenu(); + popupMenu.add(friendsItem); + popupMenu.add(exitItem); + + var image = Toolkit.getDefaultToolkit().getImage(stage.getClass().getResource("/image/trayicon.png")); + + trayIcon = new TrayIcon(image, AppName.NAME, popupMenu); + trayIcon.setImageAutoSize(true); + + systemTray = SystemTray.getSystemTray(); + + trayIcon.addMouseListener(new MouseAdapter() + { + @Override + public void mouseReleased(MouseEvent e) + { + if (e.getButton() == MouseEvent.BUTTON1) + { + Platform.runLater(() -> + { + if (stage.isShowing()) + { + stage.hide(); + } + else + { + stage.show(); + } + }); + } + else + { + super.mouseClicked(e); + } + } + }); + + try + { + systemTray.add(trayIcon); + hasSystemTray = true; + } + catch (AWTException e) + { + log.error("Failed to put system tray: {}", e.getMessage(), e); + } + } + + public boolean hasSystemTray() + { + return hasSystemTray; + } + + public static void showNotification(String message) + { + if (hasSystemTray) + { + trayIcon.displayMessage(AppName.NAME, message, TrayIcon.MessageType.NONE); + } + } + + public static void setTooltip(String message) + { + if (hasSystemTray) + { + trayIcon.setToolTip(isNotBlank(message) ? (AppName.NAME + " - " + message) : AppName.NAME); + } + } + + @PreDestroy + private void removeSystemTray() + { + if (hasSystemTray) + { + systemTray.remove(trayIcon); + hasSystemTray = false; + } + } +} diff --git a/ui/src/main/java/io/xeres/ui/support/util/UiUtils.java b/ui/src/main/java/io/xeres/ui/support/util/UiUtils.java new file mode 100644 index 000000000..72c021372 --- /dev/null +++ b/ui/src/main/java/io/xeres/ui/support/util/UiUtils.java @@ -0,0 +1,190 @@ +/* + * Copyright (c) 2019-2021 by David Gerber - https://zapek.com + * + * This file is part of Xeres. + * + * Xeres is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Xeres is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Xeres. If not, see . + */ + +package io.xeres.ui.support.util; + +import io.xeres.common.AppName; +import javafx.beans.InvalidationListener; +import javafx.css.PseudoClass; +import javafx.event.ActionEvent; +import javafx.geometry.Point2D; +import javafx.scene.Node; +import javafx.scene.control.Alert; +import javafx.scene.control.Label; +import javafx.scene.control.TextArea; +import javafx.scene.control.TextField; +import javafx.scene.image.Image; +import javafx.stage.Popup; +import javafx.stage.Stage; + +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.util.Objects; +import java.util.stream.Collectors; + +public final class UiUtils // XXX: should be renamed as TextfieldUtils perhaps... depends how much of those I end up with +{ + private UiUtils() + { + throw new UnsupportedOperationException("Utility class"); + } + + private static final PseudoClass errorTextField = PseudoClass.getPseudoClass("error"); + private static final PseudoClass warningTextField = PseudoClass.getPseudoClass("warning"); + + private static final String KEY_LISTENER = "listener"; + private static final String KEY_POPUP = "popup"; + + // XXX: fix later + //public static ErrorResponseEntity getErrorResponseEntity(Throwable throwable) + //{ + // WebClientResponseException e = (WebClientResponseException) throwable; + + // ErrorResponseEntity.Builder builder = new ErrorResponseEntity.Builder(e.getStatusCode()); + // return builder.fromJson(e.getResponseBodyAsString()); + //} + + public static void showError(TextField field, String error) + { + field.pseudoClassStateChanged(errorTextField, true); + + var label = new Label(); + label.setText(error); + label.setStyle("-fx-border-color: black; -fx-background-color: white;"); // XXX: temporary. we should probably have some proper layout somewhere + var popup = new Popup(); + popup.getContent().add(label); + var bounds = field.getBoundsInLocal(); + Point2D location = field.localToScreen(bounds.getMinX(), bounds.getMaxY()); + popup.show(field, location.getX(), location.getY()); + popup.setAutoHide(true); + field.getProperties().put(KEY_POPUP, popup); + + InvalidationListener listener = observable -> clearError(field); + field.getProperties().put(KEY_LISTENER, listener); + field.textProperty().addListener(listener); + } + + public static void clearError(TextField field) + { + InvalidationListener listener = (InvalidationListener) field.getProperties().get(KEY_LISTENER); + if (listener != null) + { + field.textProperty().removeListener(listener); + } + + var popup = (Popup) field.getProperties().get(KEY_POPUP); + if (popup != null) + { + popup.hide(); + } + field.pseudoClassStateChanged(errorTextField, false); + } + + public static void showError(TextArea textArea) + { + textArea.pseudoClassStateChanged(errorTextField, true); + } + + public static void showWarning(TextArea textArea) + { + textArea.pseudoClassStateChanged(warningTextField, true); + } + + public static void clearError(TextArea textArea) + { + textArea.pseudoClassStateChanged(errorTextField, false); + } + + public static void showAlertError(String title, String header, String message) + { + var errorAlert = new Alert(Alert.AlertType.ERROR); + if (title != null) + { + errorAlert.setTitle(title); + } + if (header != null) + { + errorAlert.setHeaderText(header); + } + errorAlert.setContentText(message); + errorAlert.showAndWait(); + } + + public static void showAlertInfo(String message) + { + showAlertInfo(AppName.NAME, null, message); + } + + public static void showAlertInfo(String title, String header, String message) + { + var errorAlert = new Alert(Alert.AlertType.INFORMATION); + errorAlert.setTitle(title); + errorAlert.setHeaderText(header); + errorAlert.setContentText(message); + errorAlert.showAndWait(); + } + + public static void setDefaultIcon(Stage stage) + { + stage.getIcons().add(new Image(Objects.requireNonNull(stage.getClass().getResourceAsStream("/image/icon.png")))); + } + + /** + * Reads a text file and returns it as a string, preserving line endings. + * + * @param in an input stream + * @return the text file + * @throws IOException I/O error + */ + public static String getResourceFileAsString(InputStream in) throws IOException + { + try (var isr = new InputStreamReader(in); + var reader = new BufferedReader(isr)) + { + return reader.lines().collect(Collectors.joining(System.lineSeparator())); + } + } + + /** + * Sets a close window actions easily, for example: + *

+	 *     closeButton.setOnAction(UiUtils::closeWindow);
+	 * 
+ * Beware because not all events contain a node (eg. events from MenuItems). + * + * @param event the event which needs a node in its source + */ + public static void closeWindow(ActionEvent event) + { + closeWindow((Node) event.getSource()); + } + + /** + * Closes a window using a node. + * + * @param node the node + */ + public static void closeWindow(Node node) + { + var stage = (Stage) node.getScene().getWindow(); + stage.close(); + } +} diff --git a/ui/src/main/java/io/xeres/ui/support/window/UiWindow.java b/ui/src/main/java/io/xeres/ui/support/window/UiWindow.java new file mode 100644 index 000000000..64b7b2954 --- /dev/null +++ b/ui/src/main/java/io/xeres/ui/support/window/UiWindow.java @@ -0,0 +1,220 @@ +/* + * Copyright (c) 2019-2021 by David Gerber - https://zapek.com + * + * This file is part of Xeres. + * + * Xeres is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Xeres is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Xeres. If not, see . + */ + +package io.xeres.ui.support.window; + +import io.xeres.common.AppName; +import io.xeres.ui.controller.WindowController; +import io.xeres.ui.support.util.UiUtils; +import javafx.fxml.FXMLLoader; +import javafx.scene.Parent; +import javafx.scene.Scene; +import javafx.stage.Modality; +import javafx.stage.Stage; +import javafx.stage.Window; +import net.rgielen.fxweaver.core.FxWeaver; + +import java.io.IOException; +import java.util.List; +import java.util.Objects; +import java.util.Optional; +import java.util.UUID; + +final class UiWindow +{ + private static FxWeaver fxWeaver; + + final Scene scene; + final Stage stage; + + static void setFxWeaver(FxWeaver fxWeaver) + { + UiWindow.fxWeaver = fxWeaver; + } + + private UiWindow(Builder builder) + { + scene = new Scene(builder.root); + scene.getStylesheets().add("/view/javafx.css"); + stage = Objects.requireNonNullElseGet(builder.stage, Stage::new); + UiUtils.setDefaultIcon(stage); + + if (builder.parent != null) + { + stage.initOwner(builder.parent); + stage.initModality(Modality.WINDOW_MODAL); + } + if (builder.localId != null) + { + if (!builder.root.getId().contains(":")) + { + throw new IllegalArgumentException("LocalId used for unique window " + builder.root.getId()); + } + String[] tokens = builder.root.getId().split(":"); + builder.root.setId(tokens[0] + ":" + builder.localId); + } + else + { + if (builder.root.getId().contains(":")) + { + throw new IllegalArgumentException("Missing localId for non unique window " + builder.root.getId()); + } + } + if (builder.userData != null) + { + builder.root.setUserData(builder.userData); + } + stage.setMinWidth(builder.minWidth); + stage.setMinHeight(builder.minHeight); + stage.setTitle(builder.title); + stage.setScene(scene); + + stage.setOnShowing(event -> builder.controller.onShowing()); + stage.setOnShown(event -> builder.controller.onShown()); + stage.setOnCloseRequest(event -> builder.controller.onCloseRequest()); + stage.setOnHiding(event -> builder.controller.onHiding()); + stage.setOnHidden(event -> builder.controller.onHidden()); + + scene.getWindow().setUserData(builder.controller); + } + + static Optional getOpenedWindow(Class controllerClass) + { + return Window.getWindows().stream() + .filter(window -> Objects.equals(window.getScene().getRoot().getId(), controllerClass.getName())) + .findFirst(); + } + + static Optional getOpenedWindow(Class controllerClass, String localId) + { + return Window.getWindows().stream() + .filter(window -> Objects.equals(window.getScene().getRoot().getId(), controllerClass.getName() + ":" + localId)) + .findFirst(); + } + + static List getOpenedWindows() + { + return Window.getWindows(); + } + + Window getWindow() + { + return scene.getWindow(); + } + + void open() + { + stage.show(); + } + + void close() + { + stage.close(); + } + + static Builder builder(Class controllerClass) + { + var parent = (Parent) fxWeaver.loadView(controllerClass); + parent.setId(controllerClass.getName()); + return new Builder(parent, fxWeaver.getBean(controllerClass)); + } + + static Builder builder(String resource, WindowController controller) + { + var fxmlLoader = new FXMLLoader(UiWindow.class.getResource(resource)); + fxmlLoader.setController(controller); + Parent parent; + try + { + parent = fxmlLoader.load(); + } + catch (IOException e) + { + throw new IllegalArgumentException("Failed to load FXML: " + e.getMessage(), e); + } + parent.setId(controller.getClass().getName() + ":" + UUID.randomUUID()); // This is a default ID to enforce uniqueness + return new Builder(parent, controller); + } + + static final class Builder + { + private Stage stage; + private final Parent root; + private final WindowController controller; + private Window parent; + private double minWidth = 240; + private double minHeight = 200; + private String title = AppName.NAME; + private String localId; + private Object userData; + + private Builder(Parent root, WindowController controller) + { + this.root = root; + this.controller = controller; + } + + Builder setParent(Window parent) + { + this.parent = parent; + return this; + } + + Builder setStage(Stage stage) + { + this.stage = stage; + return this; + } + + Builder setMinWidth(double minWidth) + { + this.minWidth = minWidth; + return this; + } + + Builder setMinHeight(double minHeight) + { + this.minHeight = minHeight; + return this; + } + + Builder setTitle(String title) + { + this.title = title; + return this; + } + + Builder setLocalId(String id) + { + this.localId = id; + return this; + } + + Builder setUserData(Object userData) + { + this.userData = userData; + return this; + } + + UiWindow build() + { + return new UiWindow(this); + } + } +} diff --git a/ui/src/main/java/io/xeres/ui/support/window/WindowManager.java b/ui/src/main/java/io/xeres/ui/support/window/WindowManager.java new file mode 100644 index 000000000..53688a358 --- /dev/null +++ b/ui/src/main/java/io/xeres/ui/support/window/WindowManager.java @@ -0,0 +1,184 @@ +/* + * Copyright (c) 2019-2021 by David Gerber - https://zapek.com + * + * This file is part of Xeres. + * + * Xeres is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Xeres is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Xeres. If not, see . + */ + +package io.xeres.ui.support.window; + +import io.xeres.common.AppName; +import io.xeres.common.message.chat.ChatMessage; +import io.xeres.ui.client.ProfileClient; +import io.xeres.ui.client.message.MessageClient; +import io.xeres.ui.controller.MainWindowController; +import io.xeres.ui.controller.about.AboutWindowController; +import io.xeres.ui.controller.account.AccountCreationWindowController; +import io.xeres.ui.controller.chat.ChatRoomCreationWindowController; +import io.xeres.ui.controller.id.AddCertificateWindowController; +import io.xeres.ui.controller.messaging.BroadcastWindowController; +import io.xeres.ui.controller.messaging.FriendsWindowController; +import io.xeres.ui.controller.messaging.MessagingWindowController; +import io.xeres.ui.controller.profile.ProfilesUiController; +import javafx.application.Platform; +import javafx.stage.Stage; +import javafx.stage.Window; +import net.rgielen.fxweaver.core.FxWeaver; +import org.springframework.stereotype.Component; + +import javax.annotation.PostConstruct; +import java.util.List; + +/** + * Class that tries to overcome the half-assed JavaFX window system. + */ +@Component +public class WindowManager +{ + private final FxWeaver fxWeaver; + private final ProfileClient profileClient; + private final MessageClient messageClient; + + public WindowManager(FxWeaver fxWeaver, ProfileClient profileClient, MessageClient messageClient) + { + this.fxWeaver = fxWeaver; + this.profileClient = profileClient; + this.messageClient = messageClient; + } + + @PostConstruct + private void initializeUiWindow() + { + UiWindow.setFxWeaver(fxWeaver); + } + + public void closeAllWindows() + { + Platform.runLater(() -> + { + // XXX: add a shutdown window (with updatable message?) + List windows = UiWindow.getOpenedWindows(); + windows.forEach(Window::hide); // XXX: race condition issue here... find out why (NoSuchElementException). Well, probably because hide() removes them :-/ + }); + } + + public void openFriends() + { + Platform.runLater(() -> + { + Window friends = UiWindow.getOpenedWindow(FriendsWindowController.class).orElse(null); + if (friends != null) + { + friends.requestFocus(); + } + else + { + UiWindow.builder(FriendsWindowController.class).build().open(); + } + }); + } + + public void openMessaging(String locationId, ChatMessage chatMessage) + { + Platform.runLater(() -> + UiWindow.getOpenedWindow(MessagingWindowController.class, locationId).ifPresentOrElse(window -> + { + window.requestFocus(); + ((MessagingWindowController) window.getUserData()).showMessage(chatMessage); + }, + () -> + { + var messaging = new MessagingWindowController(profileClient, messageClient, locationId); + + UiWindow.builder("/view/messaging/messaging.fxml", messaging) + .setLocalId(locationId) + .setUserData(chatMessage) + .build() + .open(); + })); + } + + public void openAbout(Window parent) + { + Platform.runLater(() -> + UiWindow.builder(AboutWindowController.class) + .setParent(parent) + .setTitle("About " + AppName.NAME) + .build() + .open()); + } + + public void openChatRoomCreation(Window parent) + { + Platform.runLater(() -> + UiWindow.builder(ChatRoomCreationWindowController.class) + .setParent(parent) + .setTitle("Create Chat Room") + .build() + .open()); + } + + public void openBroadcast(Window parent) + { + Platform.runLater(() -> + UiWindow.builder(BroadcastWindowController.class) + .setParent(parent) + .setTitle("Broadcast") + .setMinHeight(220) + .build() + .open()); + } + + public void openProfiles(Window parent) + { + Platform.runLater(() -> + UiWindow.builder(ProfilesUiController.class) + .setParent(parent) + .setTitle("Profiles") + .build() + .open()); + } + + public void openAddFriend(Window parent) + { + Platform.runLater(() -> + UiWindow.builder(AddCertificateWindowController.class) + .setParent(parent) + .setTitle("Add friend certificate") + .setMinHeight(380) + .build() + .open()); + } + + public void openMain(Stage stage) + { + Platform.runLater(() -> UiWindow.builder(MainWindowController.class) + .setStage(stage) + .setMinWidth(600) + .setMinHeight(400) + .build() + .open()); + } + + public void openAccountCreation(Stage stage) + { + Platform.runLater(() -> UiWindow.builder(AccountCreationWindowController.class) + .setStage(stage) + .setMinWidth(280) + .setMinHeight(240) + .build() + .open()); + } +} diff --git a/ui/src/main/resources/image/icon.png b/ui/src/main/resources/image/icon.png new file mode 100644 index 0000000000000000000000000000000000000000..6c7d68fdb5170e88b89779fb777dca4abdc86f90 GIT binary patch literal 17701 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 + + + + + + + + + + + + + + diff --git a/ui/src/main/resources/view/account/account_creation.fxml b/ui/src/main/resources/view/account/account_creation.fxml new file mode 100644 index 000000000..8e76159f7 --- /dev/null +++ b/ui/src/main/resources/view/account/account_creation.fxml @@ -0,0 +1,61 @@ + + + + + + + + + + + + + + + + + + + + + + + + +