├── .gitignore ├── application ├── src │ ├── main │ │ ├── resources │ │ │ ├── credentials │ │ │ ├── application.properties │ │ │ └── log4j2.xml │ │ └── java │ │ │ └── javasabr │ │ │ └── mqtt │ │ │ └── broker │ │ │ └── application │ │ │ ├── config │ │ │ └── package-info.java │ │ │ └── MqttBrokerApplication.java │ └── test │ │ ├── resources │ │ ├── credentials-test │ │ ├── disabled-features.properties │ │ ├── application-test.properties │ │ └── log4j2.xml │ │ └── groovy │ │ └── javasabr │ │ └── mqtt │ │ └── broker │ │ └── application │ │ ├── config │ │ └── MqttBrokerTestConfig.groovy │ │ └── service │ │ ├── NetworkMqttSessionServiceTest.groovy │ │ └── DisabledFeaturesSubscribtionServiceTest.groovy └── build.gradle ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── model ├── src │ ├── main │ │ └── java │ │ │ └── javasabr │ │ │ └── mqtt │ │ │ └── model │ │ │ ├── package-info.java │ │ │ ├── topic │ │ │ ├── package-info.java │ │ │ ├── TopicName.java │ │ │ ├── TopicFilter.java │ │ │ ├── SharedTopicFilter.java │ │ │ └── AbstractTopic.java │ │ │ ├── data │ │ │ └── type │ │ │ │ ├── package-info.java │ │ │ │ ├── MqttDataType.java │ │ │ │ └── StringPair.java │ │ │ ├── exception │ │ │ ├── package-info.java │ │ │ ├── MalformedProtocolMqttException.java │ │ │ ├── MqttException.java │ │ │ ├── CredentialsSourceException.java │ │ │ ├── InconsistentSubscriptionStateException.java │ │ │ └── ConnectionRejectException.java │ │ │ ├── message │ │ │ ├── package-info.java │ │ │ ├── SendableMqttMessage.java │ │ │ ├── ReceivableMqttMessage.java │ │ │ ├── TrackableMqttMessage.java │ │ │ └── MqttMessage.java │ │ │ ├── publishing │ │ │ └── package-info.java │ │ │ ├── reason │ │ │ └── code │ │ │ │ ├── ReasonCode.java │ │ │ │ ├── package-info.java │ │ │ │ ├── AuthenticateReasonCode.java │ │ │ │ ├── PublishReleaseReasonCode.java │ │ │ │ ├── PublishCompletedReasonCode.java │ │ │ │ ├── UnsubscribeAckReasonCode.java │ │ │ │ └── PublishAckReasonCode.java │ │ │ ├── session │ │ │ ├── package-info.java │ │ │ ├── TopicNameMapping.java │ │ │ ├── PublishRetryer.java │ │ │ ├── TrackedMessageMeta.java │ │ │ ├── TrackableMessageCallback.java │ │ │ ├── ActiveSubscriptions.java │ │ │ ├── MqttSession.java │ │ │ ├── ProcessingPublishes.java │ │ │ └── MessageTacker.java │ │ │ ├── subscriber │ │ │ ├── package-info.java │ │ │ ├── tree │ │ │ │ ├── package-info.java │ │ │ │ └── ConcurrentSubscriberTree.java │ │ │ ├── Subscriber.java │ │ │ └── SingleSubscriber.java │ │ │ ├── subscription │ │ │ ├── package-info.java │ │ │ ├── RequestedSubscription.java │ │ │ └── Subscription.java │ │ │ ├── acl │ │ │ └── Operation.java │ │ │ ├── MqttUser.java │ │ │ ├── PayloadFormat.java │ │ │ ├── SubscribeRetainHandling.java │ │ │ ├── MqttClientConnectionConfig.java │ │ │ └── QoS.java │ └── testFixtures │ │ └── groovy │ │ └── javasabr │ │ └── mqtt │ │ └── model │ │ └── subscription │ │ └── TestMqttUser.groovy └── build.gradle ├── acl-groovy-dsl ├── src │ ├── test │ │ └── resources │ │ │ └── acl │ │ │ └── config │ │ │ ├── acl-publish-only.groovy │ │ │ ├── invalid │ │ │ ├── 1.config │ │ │ ├── 3.config │ │ │ ├── 5.config │ │ │ ├── 2.config │ │ │ └── 4.config │ │ │ └── acl.groovy │ └── main │ │ └── groovy │ │ └── javasabr │ │ └── mqtt │ │ └── acl │ │ └── groovy │ │ └── dsl │ │ ├── loader │ │ ├── package-info.java │ │ └── AclRulesLoader.groovy │ │ └── builder │ │ ├── DenyPublishRuleBuilder.groovy │ │ ├── AllowPublishRuleBuilder.groovy │ │ ├── DenySubscribeRuleBuilder.groovy │ │ ├── AllowSubscribeRuleBuilder.groovy │ │ ├── AnyOfBuilder.groovy │ │ ├── PublishRuleBuilder.groovy │ │ ├── SubscribeRuleBuilder.groovy │ │ ├── RuleBuilder.groovy │ │ ├── AllOfBuilder.groovy │ │ ├── ConditionBuilder.groovy │ │ └── AclRulesBuilder.groovy └── build.gradle ├── network ├── src │ ├── main │ │ └── java │ │ │ └── javasabr │ │ │ └── mqtt │ │ │ └── network │ │ │ ├── package-info.java │ │ │ ├── impl │ │ │ ├── package-info.java │ │ │ ├── ExternalNetworkMqttUser.java │ │ │ └── InternalNetworkMqttUser.java │ │ │ ├── handler │ │ │ ├── package-info.java │ │ │ └── NetworkMqttUserReleaseHandler.java │ │ │ ├── message │ │ │ ├── package-info.java │ │ │ ├── in │ │ │ │ ├── package-info.java │ │ │ │ ├── PingRequestMqttInMessage.java │ │ │ │ ├── PingResponseMqttInMessage.java │ │ │ │ └── TrackableMqttInMessage.java │ │ │ └── out │ │ │ │ ├── package-info.java │ │ │ │ ├── PingRequestMqtt311OutMessage.java │ │ │ │ ├── PingResponseMqtt311OutMessage.java │ │ │ │ ├── DisconnectMqtt311OutMessage.java │ │ │ │ ├── PublishAckMqtt311OutMessage.java │ │ │ │ ├── TrackableMqttOutMessage.java │ │ │ │ ├── PublishCompleteMqtt311OutMessage.java │ │ │ │ ├── PublishReceivedMqtt311OutMessage.java │ │ │ │ ├── UnsubscribeAckMqtt311OutMessage.java │ │ │ │ ├── PublishReleaseMqtt311OutMessage.java │ │ │ │ ├── SubscribeMqtt311OutMessage.java │ │ │ │ └── SubscribeAckMqtt311OutMessage.java │ │ │ ├── user │ │ │ ├── package-info.java │ │ │ ├── NetworkMqttUserFactory.java │ │ │ ├── ConfigurableNetworkMqttUser.java │ │ │ └── NetworkMqttUser.java │ │ │ ├── session │ │ │ ├── package-info.java │ │ │ ├── ConfigurableNetworkMqttSession.java │ │ │ └── NetworkMqttSession.java │ │ │ ├── util │ │ │ └── ExtraErrorReasons.java │ │ │ └── MqttConnectionFactory.java │ ├── testFixtures │ │ └── resources │ │ │ └── META-INF │ │ │ └── services │ │ │ └── org.codehaus.groovy.runtime.ExtensionModule │ └── test │ │ └── groovy │ │ └── javasabr │ │ └── mqtt │ │ └── network │ │ ├── message │ │ ├── in │ │ │ └── BaseMqttInMessageTest.groovy │ │ └── out │ │ │ ├── BaseMqttOutMessageTest.groovy │ │ │ ├── DisconnectAckMqtt5OutMessageTest.groovy │ │ │ ├── ConnectMqtt311OutMessageTest.groovy │ │ │ ├── UnsubscribeAckMqtt311OutMessageTest.groovy │ │ │ ├── PublishAckMqtt311OutMessageTest.groovy │ │ │ ├── PublishReleaseMqtt311OutMessageTest.groovy │ │ │ ├── PublishCompleteMqtt311OutMessageTest.groovy │ │ │ ├── SubscribeAckMqtt311OutMessageTest.groovy │ │ │ ├── PublishReceivedMqtt311OutMessageTest.groovy │ │ │ ├── UnsubscribeAckMqtt5OutMessageTest.groovy │ │ │ ├── SubscribeAckMqtt5OutMessageTest.groovy │ │ │ ├── PublishAckMqtt5OutMessageTest.groovy │ │ │ ├── PublishReceivedMqtt5OutMessageTest.groovy │ │ │ ├── PublishReleaseMqtt5OutMessageTest.groovy │ │ │ ├── PublishCompleteMqtt5OutMessageTest.groovy │ │ │ ├── AuthenticationMqtt5OutMessageTest.groovy │ │ │ └── ConnectMqtt5OutMessageTest.groovy │ │ └── util │ │ └── MqttDataUtilsTest.groovy └── build.gradle ├── acl-engine ├── src │ ├── main │ │ └── java │ │ │ └── javasabr │ │ │ └── mqtt │ │ │ └── acl │ │ │ └── engine │ │ │ ├── model │ │ │ ├── Action.java │ │ │ ├── rule │ │ │ │ ├── package-info.java │ │ │ │ ├── Rule.java │ │ │ │ ├── DenyPublishRule.java │ │ │ │ ├── AllowPublishRule.java │ │ │ │ ├── DenySubscribeRule.java │ │ │ │ ├── AllowSubscribeRule.java │ │ │ │ └── AbstractRule.java │ │ │ ├── matcher │ │ │ │ ├── package-info.java │ │ │ │ ├── ValueMatcher.java │ │ │ │ ├── AnyValueMatcher.java │ │ │ │ ├── AnyTopicMatcher.java │ │ │ │ ├── RegexMatcher.java │ │ │ │ ├── EqualsMatcher.java │ │ │ │ ├── TopicNameMatcher.java │ │ │ │ └── TopicFilterMatcher.java │ │ │ └── condition │ │ │ │ ├── package-info.java │ │ │ │ ├── Condition.java │ │ │ │ ├── MqttUserCondition.java │ │ │ │ ├── AnyUserCondition.java │ │ │ │ ├── ClientIdCondition.java │ │ │ │ ├── UserNameCondition.java │ │ │ │ ├── IpAddressCondition.java │ │ │ │ ├── AllOfCondition.java │ │ │ │ ├── AnyOfCondition.java │ │ │ │ └── TopicCondition.java │ │ │ ├── package-info.java │ │ │ ├── exception │ │ │ └── AclConfigurationException.java │ │ │ ├── builder │ │ │ ├── ClientMatcherBuilder.java │ │ │ ├── TopicMatcherBuilder.java │ │ │ └── RuleContainerBuilder.java │ │ │ └── AclEngine.java │ ├── testFixtures │ │ └── java │ │ │ └── javasabr │ │ │ └── mqtt │ │ │ └── acl │ │ │ └── engine │ │ │ └── ConditionMatcherAware.java │ └── test │ │ └── groovy │ │ └── javasabr │ │ └── mqtt │ │ └── acl │ │ └── engine │ │ └── model │ │ └── rule │ │ └── RuleTest.groovy ├── build.gradle └── README.md ├── core-service ├── src │ ├── main │ │ └── java │ │ │ └── javasabr │ │ │ └── mqtt │ │ │ └── service │ │ │ ├── package-info.java │ │ │ ├── impl │ │ │ ├── package-info.java │ │ │ ├── DisabledAuthorizationService.java │ │ │ ├── SimpleAuthenticationService.java │ │ │ ├── ExternalNetworkMqttUserFactory.java │ │ │ ├── DefaultMqttConnectionFactory.java │ │ │ ├── AbstractCredentialSource.java │ │ │ └── FileCredentialsSource.java │ │ │ ├── session │ │ │ ├── package-info.java │ │ │ ├── impl │ │ │ │ ├── package-info.java │ │ │ │ ├── InMemoryTrackedMessageMeta.java │ │ │ │ └── InMemoryTopicNameMapping.java │ │ │ └── MqttSessionService.java │ │ │ ├── message │ │ │ ├── handler │ │ │ │ ├── MqttOutMessageHandler.java │ │ │ │ ├── package-info.java │ │ │ │ ├── impl │ │ │ │ │ ├── package-info.java │ │ │ │ │ ├── PublishAckMqttInMessageHandler.java │ │ │ │ │ ├── PublishReceiveMqttInMessageHandler.java │ │ │ │ │ ├── PublishCompleteMqttInMessageHandler.java │ │ │ │ │ ├── ProcessingOutPublishesMqttInMessageHandler.java │ │ │ │ │ └── DisconnectMqttInMessageHandler.java │ │ │ │ └── MqttInMessageHandler.java │ │ │ ├── out │ │ │ │ └── factory │ │ │ │ │ └── package-info.java │ │ │ └── validator │ │ │ │ ├── MqttInMessageFieldValidator.java │ │ │ │ ├── PublishRetainMqttInMessageFieldValidator.java │ │ │ │ ├── PublishQosMqttInMessageFieldValidator.java │ │ │ │ ├── PublishPayloadMqttInMessageFieldValidator.java │ │ │ │ ├── PublishResponseTopicMqttInMessageFieldValidator.java │ │ │ │ └── PublishMessageExpiryIntervalMqttInMessageFieldValidator.java │ │ │ ├── handler │ │ │ └── client │ │ │ │ ├── package-info.java │ │ │ │ └── ExternalNetworkMqttUserReleaseHandler.java │ │ │ ├── publish │ │ │ └── handler │ │ │ │ ├── package-info.java │ │ │ │ ├── impl │ │ │ │ ├── package-info.java │ │ │ │ ├── Qos0MqttPublishOutMessageHandler.java │ │ │ │ └── Qos0MqttPublishInMessageHandler.java │ │ │ │ ├── MqttPublishInMessageHandler.java │ │ │ │ ├── MqttPublishOutMessageHandler.java │ │ │ │ └── PublishHandlingResult.java │ │ │ ├── AuthenticationService.java │ │ │ ├── ConnectionService.java │ │ │ ├── CredentialSource.java │ │ │ ├── PublishReceivingService.java │ │ │ ├── PublishDeliveringService.java │ │ │ ├── ClientIdRegistry.java │ │ │ ├── AuthorizationService.java │ │ │ ├── MessageOutFactoryService.java │ │ │ ├── TopicService.java │ │ │ └── SubscriptionService.java │ └── test │ │ └── groovy │ │ └── javasabr │ │ └── mqtt │ │ └── service │ │ └── publish │ │ └── handler │ │ └── impl │ │ ├── QosMqttPublishOutMessageHandlerTest.groovy │ │ ├── QosMqttPublishInMessageHandlerTest.groovy │ │ └── Qos0MqttPublishOutMessageHandlerTest.groovy └── build.gradle ├── embedded ├── src │ └── main │ │ └── java │ │ └── javasabr │ │ └── mqtt │ │ └── broker │ │ └── library │ │ └── MqttBrokerConfiguration.java └── build.gradle ├── acl-service ├── src │ └── main │ │ └── java │ │ └── javasabr │ │ └── mqtt │ │ └── acl │ │ └── service │ │ ├── package-info.java │ │ ├── AclEngineBasedAuthorizationService.java │ │ └── conifg │ │ └── GroovyDslBasedAclServiceSpringConfig.java └── build.gradle ├── lombok.config ├── test-support ├── src │ └── main │ │ └── groovy │ │ └── javasabr │ │ └── mqtt │ │ └── test │ │ └── support │ │ └── UnitSpecification.groovy └── build.gradle ├── base ├── build.gradle └── src │ ├── test │ └── groovy │ │ └── javasabr │ │ └── mqtt │ │ └── base │ │ └── util │ │ └── DebugUtilsTest.groovy │ └── main │ └── java │ └── javasabr │ └── mqtt │ └── base │ └── util │ └── ReactorUtils.java ├── settings.gradle └── test-coverage └── build.gradle /.gitignore: -------------------------------------------------------------------------------- 1 | /.idea/ 2 | **/.gradle/ 3 | **/build/ 4 | /out/ 5 | -------------------------------------------------------------------------------- /application/src/main/resources/credentials: -------------------------------------------------------------------------------- 1 | user=password 2 | -------------------------------------------------------------------------------- /application/src/test/resources/credentials-test: -------------------------------------------------------------------------------- 1 | user=password 2 | user1=password 3 | -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JavaSaBr/mqtt-broker/HEAD/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /application/src/main/resources/application.properties: -------------------------------------------------------------------------------- 1 | authentication.allow.anonymous=false 2 | credentials.source.file.name=credentials 3 | -------------------------------------------------------------------------------- /model/src/main/java/javasabr/mqtt/model/package-info.java: -------------------------------------------------------------------------------- 1 | @NullMarked 2 | package javasabr.mqtt.model; 3 | 4 | import org.jspecify.annotations.NullMarked; -------------------------------------------------------------------------------- /acl-groovy-dsl/src/test/resources/acl/config/acl-publish-only.groovy: -------------------------------------------------------------------------------- 1 | package acl.config 2 | 3 | denyPublish { 4 | anyOf() 5 | topicName anyone() 6 | } 7 | -------------------------------------------------------------------------------- /network/src/main/java/javasabr/mqtt/network/package-info.java: -------------------------------------------------------------------------------- 1 | @NullMarked 2 | package javasabr.mqtt.network; 3 | 4 | import org.jspecify.annotations.NullMarked; -------------------------------------------------------------------------------- /acl-engine/src/main/java/javasabr/mqtt/acl/engine/model/Action.java: -------------------------------------------------------------------------------- 1 | package javasabr.mqtt.acl.engine.model; 2 | 3 | public enum Action { 4 | ALLOW, DENY 5 | } 6 | -------------------------------------------------------------------------------- /core-service/src/main/java/javasabr/mqtt/service/package-info.java: -------------------------------------------------------------------------------- 1 | @NullMarked 2 | package javasabr.mqtt.service; 3 | 4 | import org.jspecify.annotations.NullMarked; -------------------------------------------------------------------------------- /model/src/main/java/javasabr/mqtt/model/topic/package-info.java: -------------------------------------------------------------------------------- 1 | @NullMarked 2 | package javasabr.mqtt.model.topic; 3 | 4 | import org.jspecify.annotations.NullMarked; -------------------------------------------------------------------------------- /embedded/src/main/java/javasabr/mqtt/broker/library/MqttBrokerConfiguration.java: -------------------------------------------------------------------------------- 1 | package javasabr.mqtt.broker.library; 2 | 3 | public class MqttBrokerConfiguration {} 4 | -------------------------------------------------------------------------------- /network/src/main/java/javasabr/mqtt/network/impl/package-info.java: -------------------------------------------------------------------------------- 1 | @NullMarked 2 | package javasabr.mqtt.network.impl; 3 | 4 | import org.jspecify.annotations.NullMarked; -------------------------------------------------------------------------------- /acl-engine/src/main/java/javasabr/mqtt/acl/engine/package-info.java: -------------------------------------------------------------------------------- 1 | @NullMarked 2 | package javasabr.mqtt.acl.engine; 3 | 4 | import org.jspecify.annotations.NullMarked; 5 | -------------------------------------------------------------------------------- /acl-service/src/main/java/javasabr/mqtt/acl/service/package-info.java: -------------------------------------------------------------------------------- 1 | @NullMarked 2 | package javasabr.mqtt.acl.service; 3 | 4 | import org.jspecify.annotations.NullMarked; 5 | -------------------------------------------------------------------------------- /core-service/src/main/java/javasabr/mqtt/service/impl/package-info.java: -------------------------------------------------------------------------------- 1 | @NullMarked 2 | package javasabr.mqtt.service.impl; 3 | 4 | import org.jspecify.annotations.NullMarked; -------------------------------------------------------------------------------- /model/src/main/java/javasabr/mqtt/model/data/type/package-info.java: -------------------------------------------------------------------------------- 1 | @NullMarked 2 | package javasabr.mqtt.model.data.type; 3 | 4 | import org.jspecify.annotations.NullMarked; -------------------------------------------------------------------------------- /model/src/main/java/javasabr/mqtt/model/exception/package-info.java: -------------------------------------------------------------------------------- 1 | @NullMarked 2 | package javasabr.mqtt.model.exception; 3 | 4 | import org.jspecify.annotations.NullMarked; -------------------------------------------------------------------------------- /model/src/main/java/javasabr/mqtt/model/message/package-info.java: -------------------------------------------------------------------------------- 1 | @NullMarked 2 | package javasabr.mqtt.model.message; 3 | 4 | import org.jspecify.annotations.NullMarked; 5 | -------------------------------------------------------------------------------- /model/src/main/java/javasabr/mqtt/model/publishing/package-info.java: -------------------------------------------------------------------------------- 1 | @NullMarked 2 | package javasabr.mqtt.model.publishing; 3 | 4 | import org.jspecify.annotations.NullMarked; -------------------------------------------------------------------------------- /model/src/main/java/javasabr/mqtt/model/reason/code/ReasonCode.java: -------------------------------------------------------------------------------- 1 | package javasabr.mqtt.model.reason.code; 2 | 3 | public interface ReasonCode { 4 | int code(); 5 | } 6 | -------------------------------------------------------------------------------- /model/src/main/java/javasabr/mqtt/model/reason/code/package-info.java: -------------------------------------------------------------------------------- 1 | @NullMarked 2 | package javasabr.mqtt.model.reason.code; 3 | 4 | import org.jspecify.annotations.NullMarked; -------------------------------------------------------------------------------- /model/src/main/java/javasabr/mqtt/model/session/package-info.java: -------------------------------------------------------------------------------- 1 | @NullMarked 2 | package javasabr.mqtt.model.session; 3 | 4 | import org.jspecify.annotations.NullMarked; 5 | -------------------------------------------------------------------------------- /model/src/main/java/javasabr/mqtt/model/subscriber/package-info.java: -------------------------------------------------------------------------------- 1 | @NullMarked 2 | package javasabr.mqtt.model.subscriber; 3 | 4 | import org.jspecify.annotations.NullMarked; -------------------------------------------------------------------------------- /network/src/main/java/javasabr/mqtt/network/handler/package-info.java: -------------------------------------------------------------------------------- 1 | @NullMarked 2 | package javasabr.mqtt.network.handler; 3 | 4 | import org.jspecify.annotations.NullMarked; -------------------------------------------------------------------------------- /network/src/main/java/javasabr/mqtt/network/message/package-info.java: -------------------------------------------------------------------------------- 1 | @NullMarked 2 | package javasabr.mqtt.network.message; 3 | 4 | import org.jspecify.annotations.NullMarked; -------------------------------------------------------------------------------- /network/src/main/java/javasabr/mqtt/network/user/package-info.java: -------------------------------------------------------------------------------- 1 | @NullMarked 2 | package javasabr.mqtt.network.user; 3 | 4 | import org.jspecify.annotations.NullMarked; 5 | -------------------------------------------------------------------------------- /acl-groovy-dsl/src/test/resources/acl/config/invalid/1.config: -------------------------------------------------------------------------------- 1 | allowPublish { 2 | anyOf { 3 | userName eq("sensor") 4 | } 5 | anyOf() 6 | topicName eq("topic") 7 | } 8 | -------------------------------------------------------------------------------- /acl-groovy-dsl/src/test/resources/acl/config/invalid/3.config: -------------------------------------------------------------------------------- 1 | allowPublish { 2 | allOf { 3 | userName eq("sensor2"), eq("sensor2") 4 | } 5 | topicName eq("topic") 6 | } 7 | -------------------------------------------------------------------------------- /core-service/src/main/java/javasabr/mqtt/service/session/package-info.java: -------------------------------------------------------------------------------- 1 | @NullMarked 2 | package javasabr.mqtt.service.session; 3 | 4 | import org.jspecify.annotations.NullMarked; -------------------------------------------------------------------------------- /model/src/main/java/javasabr/mqtt/model/message/SendableMqttMessage.java: -------------------------------------------------------------------------------- 1 | package javasabr.mqtt.model.message; 2 | 3 | public interface SendableMqttMessage extends MqttMessage {} 4 | -------------------------------------------------------------------------------- /network/src/main/java/javasabr/mqtt/network/message/in/package-info.java: -------------------------------------------------------------------------------- 1 | @NullMarked 2 | package javasabr.mqtt.network.message.in; 3 | 4 | import org.jspecify.annotations.NullMarked; -------------------------------------------------------------------------------- /network/src/main/java/javasabr/mqtt/network/session/package-info.java: -------------------------------------------------------------------------------- 1 | @NullMarked 2 | package javasabr.mqtt.network.session; 3 | 4 | import org.jspecify.annotations.NullMarked; 5 | -------------------------------------------------------------------------------- /model/src/main/java/javasabr/mqtt/model/message/ReceivableMqttMessage.java: -------------------------------------------------------------------------------- 1 | package javasabr.mqtt.model.message; 2 | 3 | public interface ReceivableMqttMessage extends MqttMessage {} 4 | -------------------------------------------------------------------------------- /model/src/main/java/javasabr/mqtt/model/subscriber/tree/package-info.java: -------------------------------------------------------------------------------- 1 | @NullMarked 2 | package javasabr.mqtt.model.subscriber.tree; 3 | 4 | import org.jspecify.annotations.NullMarked; -------------------------------------------------------------------------------- /model/src/main/java/javasabr/mqtt/model/subscription/package-info.java: -------------------------------------------------------------------------------- 1 | @NullMarked 2 | package javasabr.mqtt.model.subscription; 3 | 4 | import org.jspecify.annotations.NullMarked; 5 | -------------------------------------------------------------------------------- /network/src/main/java/javasabr/mqtt/network/message/out/package-info.java: -------------------------------------------------------------------------------- 1 | @NullMarked 2 | package javasabr.mqtt.network.message.out; 3 | 4 | import org.jspecify.annotations.NullMarked; -------------------------------------------------------------------------------- /application/src/test/resources/disabled-features.properties: -------------------------------------------------------------------------------- 1 | mqtt.external.connection.shared.subscription.available=false 2 | mqtt.external.connection.wildcard.subscription.available=false 3 | -------------------------------------------------------------------------------- /core-service/src/main/java/javasabr/mqtt/service/message/handler/MqttOutMessageHandler.java: -------------------------------------------------------------------------------- 1 | package javasabr.mqtt.service.message.handler; 2 | 3 | public interface MqttOutMessageHandler {} 4 | -------------------------------------------------------------------------------- /core-service/src/main/java/javasabr/mqtt/service/session/impl/package-info.java: -------------------------------------------------------------------------------- 1 | @NullMarked 2 | package javasabr.mqtt.service.session.impl; 3 | 4 | import org.jspecify.annotations.NullMarked; -------------------------------------------------------------------------------- /acl-engine/src/main/java/javasabr/mqtt/acl/engine/model/rule/package-info.java: -------------------------------------------------------------------------------- 1 | @NullMarked 2 | package javasabr.mqtt.acl.engine.model.rule; 3 | 4 | import org.jspecify.annotations.NullMarked; 5 | -------------------------------------------------------------------------------- /acl-groovy-dsl/src/test/resources/acl/config/invalid/5.config: -------------------------------------------------------------------------------- 1 | allowPublish { 2 | allOf { 3 | userName eq("sensor1") 4 | userName eq("sensor2") 5 | } 6 | topicName eq("topic") 7 | } 8 | -------------------------------------------------------------------------------- /core-service/src/main/java/javasabr/mqtt/service/handler/client/package-info.java: -------------------------------------------------------------------------------- 1 | @NullMarked 2 | package javasabr.mqtt.service.handler.client; 3 | 4 | import org.jspecify.annotations.NullMarked; -------------------------------------------------------------------------------- /core-service/src/main/java/javasabr/mqtt/service/message/handler/package-info.java: -------------------------------------------------------------------------------- 1 | @NullMarked 2 | package javasabr.mqtt.service.message.handler; 3 | 4 | import org.jspecify.annotations.NullMarked; -------------------------------------------------------------------------------- /core-service/src/main/java/javasabr/mqtt/service/publish/handler/package-info.java: -------------------------------------------------------------------------------- 1 | @NullMarked 2 | package javasabr.mqtt.service.publish.handler; 3 | 4 | import org.jspecify.annotations.NullMarked; -------------------------------------------------------------------------------- /acl-engine/src/main/java/javasabr/mqtt/acl/engine/model/matcher/package-info.java: -------------------------------------------------------------------------------- 1 | @NullMarked 2 | package javasabr.mqtt.acl.engine.model.matcher; 3 | 4 | import org.jspecify.annotations.NullMarked; 5 | -------------------------------------------------------------------------------- /acl-groovy-dsl/src/main/groovy/javasabr/mqtt/acl/groovy/dsl/loader/package-info.java: -------------------------------------------------------------------------------- 1 | @NullMarked 2 | package javasabr.mqtt.acl.groovy.dsl.loader; 3 | 4 | import org.jspecify.annotations.NullMarked; 5 | -------------------------------------------------------------------------------- /application/src/main/java/javasabr/mqtt/broker/application/config/package-info.java: -------------------------------------------------------------------------------- 1 | @NullMarked 2 | package javasabr.mqtt.broker.application.config; 3 | 4 | import org.jspecify.annotations.NullMarked; -------------------------------------------------------------------------------- /lombok.config: -------------------------------------------------------------------------------- 1 | lombok.log.custom.declaration = javasabr.rlib.logger.api.Logger javasabr.rlib.logger.api.LoggerManager.getLogger(TYPE) 2 | lombok.accessors.fluent=true 3 | lombok.accessors.chain=false 4 | -------------------------------------------------------------------------------- /acl-engine/src/main/java/javasabr/mqtt/acl/engine/model/condition/package-info.java: -------------------------------------------------------------------------------- 1 | @NullMarked 2 | package javasabr.mqtt.acl.engine.model.condition; 3 | 4 | import org.jspecify.annotations.NullMarked; 5 | -------------------------------------------------------------------------------- /core-service/src/main/java/javasabr/mqtt/service/message/handler/impl/package-info.java: -------------------------------------------------------------------------------- 1 | @NullMarked 2 | package javasabr.mqtt.service.message.handler.impl; 3 | 4 | import org.jspecify.annotations.NullMarked; -------------------------------------------------------------------------------- /core-service/src/main/java/javasabr/mqtt/service/message/out/factory/package-info.java: -------------------------------------------------------------------------------- 1 | @NullMarked 2 | package javasabr.mqtt.service.message.out.factory; 3 | 4 | import org.jspecify.annotations.NullMarked; -------------------------------------------------------------------------------- /core-service/src/main/java/javasabr/mqtt/service/publish/handler/impl/package-info.java: -------------------------------------------------------------------------------- 1 | @NullMarked 2 | package javasabr.mqtt.service.publish.handler.impl; 3 | 4 | import org.jspecify.annotations.NullMarked; -------------------------------------------------------------------------------- /model/src/main/java/javasabr/mqtt/model/message/TrackableMqttMessage.java: -------------------------------------------------------------------------------- 1 | package javasabr.mqtt.model.message; 2 | 3 | public interface TrackableMqttMessage extends MqttMessage { 4 | 5 | int messageId(); 6 | } 7 | -------------------------------------------------------------------------------- /test-support/src/main/groovy/javasabr/mqtt/test/support/UnitSpecification.groovy: -------------------------------------------------------------------------------- 1 | package javasabr.mqtt.test.support 2 | 3 | import spock.lang.Specification 4 | 5 | class UnitSpecification extends Specification { 6 | } 7 | -------------------------------------------------------------------------------- /acl-groovy-dsl/src/test/resources/acl/config/invalid/2.config: -------------------------------------------------------------------------------- 1 | allowPublish { 2 | anyOf { 3 | userName eq("sensor") 4 | } 5 | allOf { 6 | userName eq("user") 7 | } 8 | topicName eq("topic") 9 | } 10 | -------------------------------------------------------------------------------- /network/src/testFixtures/resources/META-INF/services/org.codehaus.groovy.runtime.ExtensionModule: -------------------------------------------------------------------------------- 1 | moduleName=network-testFixtures 2 | moduleVersion=1.0 3 | extensionClasses=javasabr.mqtt.network.SpecificationNetworkExtensions 4 | -------------------------------------------------------------------------------- /network/src/main/java/javasabr/mqtt/network/util/ExtraErrorReasons.java: -------------------------------------------------------------------------------- 1 | package javasabr.mqtt.network.util; 2 | 3 | public interface ExtraErrorReasons { 4 | String SESSION_IS_ALREADY_CLOSED = "Session is already closed"; 5 | } 6 | -------------------------------------------------------------------------------- /acl-groovy-dsl/src/test/resources/acl/config/invalid/4.config: -------------------------------------------------------------------------------- 1 | allowPublish { 2 | anyOf { 3 | allOf { 4 | allOf { 5 | userName eq("user") 6 | } 7 | } 8 | } 9 | topicName eq("/topic1"), eq("/topic2/temp") 10 | } 11 | -------------------------------------------------------------------------------- /core-service/src/main/java/javasabr/mqtt/service/AuthenticationService.java: -------------------------------------------------------------------------------- 1 | package javasabr.mqtt.service; 2 | 3 | import reactor.core.publisher.Mono; 4 | 5 | public interface AuthenticationService { 6 | Mono auth(String userName, byte[] password); 7 | } 8 | -------------------------------------------------------------------------------- /network/src/test/groovy/javasabr/mqtt/network/message/in/BaseMqttInMessageTest.groovy: -------------------------------------------------------------------------------- 1 | package javasabr.mqtt.network.message.in 2 | 3 | import javasabr.mqtt.network.NetworkUnitSpecification 4 | 5 | class BaseMqttInMessageTest extends NetworkUnitSpecification { 6 | } 7 | -------------------------------------------------------------------------------- /acl-engine/src/main/java/javasabr/mqtt/acl/engine/model/condition/Condition.java: -------------------------------------------------------------------------------- 1 | package javasabr.mqtt.acl.engine.model.condition; 2 | 3 | import org.jspecify.annotations.Nullable; 4 | 5 | public interface Condition { 6 | 7 | boolean test(@Nullable T value); 8 | } 9 | -------------------------------------------------------------------------------- /application/src/test/resources/application-test.properties: -------------------------------------------------------------------------------- 1 | authentication.allow.anonymous=true 2 | credentials.source.file.name=credentials-test 3 | mqtt.external.connection.shared.subscription.available=true 4 | mqtt.external.connection.wildcard.subscription.available=true 5 | -------------------------------------------------------------------------------- /network/src/main/java/javasabr/mqtt/network/session/ConfigurableNetworkMqttSession.java: -------------------------------------------------------------------------------- 1 | package javasabr.mqtt.network.session; 2 | 3 | public interface ConfigurableNetworkMqttSession extends NetworkMqttSession { 4 | 5 | void expirationTime(long expirationTime); 6 | } 7 | -------------------------------------------------------------------------------- /acl-engine/src/main/java/javasabr/mqtt/acl/engine/model/matcher/ValueMatcher.java: -------------------------------------------------------------------------------- 1 | package javasabr.mqtt.acl.engine.model.matcher; 2 | 3 | public interface ValueMatcher { 4 | 5 | ValueMatcher MATCH_ANY = new AnyValueMatcher(); 6 | 7 | boolean test(T value); 8 | } 9 | -------------------------------------------------------------------------------- /core-service/src/main/java/javasabr/mqtt/service/ConnectionService.java: -------------------------------------------------------------------------------- 1 | package javasabr.mqtt.service; 2 | 3 | import javasabr.mqtt.network.MqttConnection; 4 | 5 | public interface ConnectionService { 6 | 7 | void processAcceptedConnection(MqttConnection connection); 8 | } 9 | -------------------------------------------------------------------------------- /network/src/test/groovy/javasabr/mqtt/network/message/out/BaseMqttOutMessageTest.groovy: -------------------------------------------------------------------------------- 1 | package javasabr.mqtt.network.message.out 2 | 3 | import javasabr.mqtt.network.NetworkUnitSpecification 4 | 5 | class BaseMqttOutMessageTest extends NetworkUnitSpecification { 6 | 7 | } 8 | -------------------------------------------------------------------------------- /acl-engine/build.gradle: -------------------------------------------------------------------------------- 1 | plugins { 2 | id("groovy") 3 | id("java-library") 4 | id("configure-java") 5 | } 6 | 7 | dependencies { 8 | api projects.model 9 | 10 | testImplementation projects.testSupport 11 | testImplementation testFixtures(projects.model) 12 | } 13 | -------------------------------------------------------------------------------- /model/src/main/java/javasabr/mqtt/model/data/type/MqttDataType.java: -------------------------------------------------------------------------------- 1 | package javasabr.mqtt.model.data.type; 2 | 3 | public enum MqttDataType { 4 | BYTE, 5 | SHORT, 6 | INTEGER, 7 | MULTI_BYTE_INTEGER, 8 | BINARY, 9 | UTF_8_STRING, 10 | UTF_8_STRING_PAIR 11 | } 12 | -------------------------------------------------------------------------------- /acl-service/build.gradle: -------------------------------------------------------------------------------- 1 | plugins { 2 | id("java-library") 3 | id("configure-java") 4 | } 5 | 6 | dependencies { 7 | api projects.aclEngine 8 | api projects.coreService 9 | api libs.springboot.starter.autoconfigure 10 | compileOnlyApi projects.aclGroovyDsl 11 | } 12 | -------------------------------------------------------------------------------- /acl-engine/src/main/java/javasabr/mqtt/acl/engine/exception/AclConfigurationException.java: -------------------------------------------------------------------------------- 1 | package javasabr.mqtt.acl.engine.exception; 2 | 3 | public class AclConfigurationException extends RuntimeException { 4 | 5 | public AclConfigurationException(String message) { 6 | super(message); 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /network/src/main/java/javasabr/mqtt/network/user/NetworkMqttUserFactory.java: -------------------------------------------------------------------------------- 1 | package javasabr.mqtt.network.user; 2 | 3 | import javasabr.mqtt.network.MqttConnection; 4 | 5 | public interface NetworkMqttUserFactory { 6 | 7 | ConfigurableNetworkMqttUser createNetworkUser(MqttConnection connection); 8 | } 9 | -------------------------------------------------------------------------------- /core-service/src/main/java/javasabr/mqtt/service/CredentialSource.java: -------------------------------------------------------------------------------- 1 | package javasabr.mqtt.service; 2 | 3 | import reactor.core.publisher.Mono; 4 | 5 | public interface CredentialSource { 6 | 7 | Mono check(String user, byte[] pass); 8 | 9 | Mono check(byte[] pass); 10 | } 11 | -------------------------------------------------------------------------------- /acl-engine/src/main/java/javasabr/mqtt/acl/engine/model/matcher/AnyValueMatcher.java: -------------------------------------------------------------------------------- 1 | package javasabr.mqtt.acl.engine.model.matcher; 2 | 3 | public record AnyValueMatcher() implements ValueMatcher { 4 | 5 | @Override 6 | public boolean test(String value) { 7 | return true; 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /base/build.gradle: -------------------------------------------------------------------------------- 1 | plugins { 2 | id("java-library") 3 | id("configure-java") 4 | id("groovy") 5 | } 6 | 7 | dependencies { 8 | api libs.jackson.core 9 | api libs.jackson.databind 10 | api libs.project.reactor.core 11 | api libs.rlib.collections 12 | testImplementation projects.testSupport 13 | } -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | distributionBase=GRADLE_USER_HOME 2 | distributionPath=wrapper/dists 3 | distributionUrl=https\://services.gradle.org/distributions/gradle-9.1.0-all.zip 4 | networkTimeout=10000 5 | validateDistributionUrl=true 6 | zipStoreBase=GRADLE_USER_HOME 7 | zipStorePath=wrapper/dists 8 | -------------------------------------------------------------------------------- /model/build.gradle: -------------------------------------------------------------------------------- 1 | plugins { 2 | id("java-library") 3 | id("configure-java") 4 | id("groovy") 5 | } 6 | 7 | description = "Define most of domain classes" 8 | 9 | dependencies { 10 | api projects.base 11 | 12 | testImplementation projects.testSupport 13 | testFixturesApi projects.testSupport 14 | } 15 | -------------------------------------------------------------------------------- /acl-groovy-dsl/build.gradle: -------------------------------------------------------------------------------- 1 | plugins { 2 | id("groovy") 3 | id("java-library") 4 | id("configure-java") 5 | } 6 | 7 | dependencies { 8 | api projects.aclEngine 9 | api libs.groovy.core 10 | api libs.rlib.collections 11 | 12 | testImplementation testFixtures(projects.model) 13 | testImplementation projects.testSupport 14 | } 15 | -------------------------------------------------------------------------------- /core-service/src/main/java/javasabr/mqtt/service/PublishReceivingService.java: -------------------------------------------------------------------------------- 1 | package javasabr.mqtt.service; 2 | 3 | import javasabr.mqtt.model.publishing.Publish; 4 | import javasabr.mqtt.network.user.NetworkMqttUser; 5 | 6 | public interface PublishReceivingService { 7 | 8 | void processPublish(NetworkMqttUser user, Publish publish); 9 | } 10 | -------------------------------------------------------------------------------- /acl-engine/src/main/java/javasabr/mqtt/acl/engine/model/condition/MqttUserCondition.java: -------------------------------------------------------------------------------- 1 | package javasabr.mqtt.acl.engine.model.condition; 2 | 3 | import javasabr.mqtt.model.MqttUser; 4 | 5 | public interface MqttUserCondition { 6 | 7 | MqttUserCondition MATCH_ANY = new AnyUserCondition(); 8 | 9 | boolean test(MqttUser requestedUser); 10 | } 11 | -------------------------------------------------------------------------------- /core-service/src/test/groovy/javasabr/mqtt/service/publish/handler/impl/QosMqttPublishOutMessageHandlerTest.groovy: -------------------------------------------------------------------------------- 1 | package javasabr.mqtt.service.publish.handler.impl 2 | 3 | import javasabr.mqtt.service.IntegrationServiceSpecification 4 | 5 | abstract class QosMqttPublishOutMessageHandlerTest extends IntegrationServiceSpecification { 6 | static { 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /core-service/src/main/java/javasabr/mqtt/service/PublishDeliveringService.java: -------------------------------------------------------------------------------- 1 | package javasabr.mqtt.service; 2 | 3 | import javasabr.mqtt.model.publishing.Publish; 4 | import javasabr.mqtt.model.subscriber.SingleSubscriber; 5 | 6 | public interface PublishDeliveringService { 7 | 8 | void startDelivering(Publish publish, SingleSubscriber subscriber); 9 | } 10 | -------------------------------------------------------------------------------- /network/src/main/java/javasabr/mqtt/network/handler/NetworkMqttUserReleaseHandler.java: -------------------------------------------------------------------------------- 1 | package javasabr.mqtt.network.handler; 2 | 3 | import javasabr.mqtt.network.user.ConfigurableNetworkMqttUser; 4 | import reactor.core.publisher.Mono; 5 | 6 | public interface NetworkMqttUserReleaseHandler { 7 | 8 | Mono release(ConfigurableNetworkMqttUser user); 9 | } 10 | -------------------------------------------------------------------------------- /acl-engine/src/main/java/javasabr/mqtt/acl/engine/model/condition/AnyUserCondition.java: -------------------------------------------------------------------------------- 1 | package javasabr.mqtt.acl.engine.model.condition; 2 | 3 | import javasabr.mqtt.model.MqttUser; 4 | 5 | public record AnyUserCondition() implements MqttUserCondition { 6 | 7 | @Override 8 | public boolean test(MqttUser requestedUser) { 9 | return true; 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /model/src/main/java/javasabr/mqtt/model/exception/MalformedProtocolMqttException.java: -------------------------------------------------------------------------------- 1 | package javasabr.mqtt.model.exception; 2 | 3 | import lombok.NoArgsConstructor; 4 | 5 | @NoArgsConstructor 6 | public class MalformedProtocolMqttException extends MqttException { 7 | public MalformedProtocolMqttException(String message) { 8 | super(message); 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /network/src/main/java/javasabr/mqtt/network/MqttConnectionFactory.java: -------------------------------------------------------------------------------- 1 | package javasabr.mqtt.network; 2 | 3 | import java.nio.channels.AsynchronousSocketChannel; 4 | import javasabr.rlib.network.Network; 5 | 6 | public interface MqttConnectionFactory { 7 | 8 | MqttConnection newConnection(Network network, AsynchronousSocketChannel channel); 9 | } 10 | -------------------------------------------------------------------------------- /model/src/main/java/javasabr/mqtt/model/exception/MqttException.java: -------------------------------------------------------------------------------- 1 | package javasabr.mqtt.model.exception; 2 | 3 | public class MqttException extends RuntimeException { 4 | 5 | public MqttException() {} 6 | 7 | public MqttException(String message) { 8 | super(message); 9 | } 10 | 11 | public MqttException(Throwable cause) { 12 | super(cause); 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /model/src/main/java/javasabr/mqtt/model/message/MqttMessage.java: -------------------------------------------------------------------------------- 1 | package javasabr.mqtt.model.message; 2 | 3 | import javasabr.mqtt.model.data.type.StringPair; 4 | import javasabr.rlib.collections.array.Array; 5 | 6 | public interface MqttMessage { 7 | 8 | Array EMPTY_USER_PROPERTIES = Array.empty(StringPair.class); 9 | 10 | MqttMessageType messageType(); 11 | } 12 | -------------------------------------------------------------------------------- /model/src/main/java/javasabr/mqtt/model/session/TopicNameMapping.java: -------------------------------------------------------------------------------- 1 | package javasabr.mqtt.model.session; 2 | 3 | import javasabr.mqtt.model.topic.TopicName; 4 | import org.jspecify.annotations.Nullable; 5 | 6 | public interface TopicNameMapping { 7 | 8 | void update(int topicAlias, TopicName topicName); 9 | 10 | @Nullable 11 | TopicName resolve(int topicAlias); 12 | } 13 | -------------------------------------------------------------------------------- /acl-engine/src/main/java/javasabr/mqtt/acl/engine/model/matcher/AnyTopicMatcher.java: -------------------------------------------------------------------------------- 1 | package javasabr.mqtt.acl.engine.model.matcher; 2 | 3 | import javasabr.mqtt.model.topic.AbstractTopic; 4 | 5 | public record AnyTopicMatcher() implements ValueMatcher { 6 | 7 | @Override 8 | public boolean test(AbstractTopic requestedTopic) { 9 | return true; 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /acl-engine/src/main/java/javasabr/mqtt/acl/engine/model/matcher/RegexMatcher.java: -------------------------------------------------------------------------------- 1 | package javasabr.mqtt.acl.engine.model.matcher; 2 | 3 | import java.util.regex.Pattern; 4 | 5 | public record RegexMatcher(Pattern pattern) implements ValueMatcher { 6 | 7 | @Override 8 | public boolean test(String value) { 9 | return pattern.matcher(value).matches(); 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /acl-engine/src/main/java/javasabr/mqtt/acl/engine/model/matcher/EqualsMatcher.java: -------------------------------------------------------------------------------- 1 | package javasabr.mqtt.acl.engine.model.matcher; 2 | 3 | import java.util.Objects; 4 | 5 | public record EqualsMatcher(String expectedValue) implements ValueMatcher { 6 | 7 | @Override 8 | public boolean test(String value) { 9 | return Objects.equals(expectedValue, value); 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /core-service/src/main/java/javasabr/mqtt/service/ClientIdRegistry.java: -------------------------------------------------------------------------------- 1 | package javasabr.mqtt.service; 2 | 3 | import reactor.core.publisher.Mono; 4 | 5 | public interface ClientIdRegistry { 6 | 7 | Mono register(String clientId); 8 | 9 | Mono unregister(String clientId); 10 | 11 | boolean validate(String clientId); 12 | 13 | Mono generate(); 14 | } 15 | -------------------------------------------------------------------------------- /model/src/main/java/javasabr/mqtt/model/exception/CredentialsSourceException.java: -------------------------------------------------------------------------------- 1 | package javasabr.mqtt.model.exception; 2 | 3 | public class CredentialsSourceException extends RuntimeException { 4 | 5 | public CredentialsSourceException(String message) { 6 | super(message); 7 | } 8 | 9 | public CredentialsSourceException(Throwable cause) { 10 | super(cause); 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /model/src/main/java/javasabr/mqtt/model/session/PublishRetryer.java: -------------------------------------------------------------------------------- 1 | package javasabr.mqtt.model.session; 2 | 3 | import javasabr.mqtt.model.MqttUser; 4 | import javasabr.mqtt.model.publishing.Publish; 5 | 6 | public interface PublishRetryer { 7 | 8 | PublishRetryer NO_OPS = (owner, session, publish) -> {}; 9 | 10 | void retry(MqttUser user, MqttSession session, Publish publish); 11 | } 12 | -------------------------------------------------------------------------------- /settings.gradle: -------------------------------------------------------------------------------- 1 | enableFeaturePreview("TYPESAFE_PROJECT_ACCESSORS") 2 | 3 | rootProject.name = 'MQTT-Broker' 4 | 5 | include(":base") 6 | include(":model") 7 | include(":network") 8 | include(":core-service") 9 | include(":application") 10 | include(":embedded") 11 | include(":test-support") 12 | include(":test-coverage") 13 | include(":acl-groovy-dsl") 14 | include(":acl-engine") 15 | include(":acl-service") 16 | -------------------------------------------------------------------------------- /core-service/build.gradle: -------------------------------------------------------------------------------- 1 | plugins { 2 | id("java-library") 3 | id("configure-java") 4 | id("groovy") 5 | } 6 | 7 | description = "Provides interfaces and minimal implementation of all required services for MQTT Broker" 8 | 9 | dependencies { 10 | api projects.network 11 | api projects.aclEngine 12 | 13 | testImplementation projects.testSupport 14 | testImplementation testFixtures(projects.network) 15 | } -------------------------------------------------------------------------------- /test-coverage/build.gradle: -------------------------------------------------------------------------------- 1 | plugins { 2 | id("configure-java") 3 | id("jacoco-report-aggregation") 4 | } 5 | 6 | description = "Aggreggate test coverages from all modules" 7 | 8 | dependencies { 9 | jacocoAggregation projects.base 10 | jacocoAggregation projects.application 11 | jacocoAggregation projects.model 12 | jacocoAggregation projects.network 13 | jacocoAggregation projects.coreService 14 | } 15 | -------------------------------------------------------------------------------- /model/src/main/java/javasabr/mqtt/model/session/TrackedMessageMeta.java: -------------------------------------------------------------------------------- 1 | package javasabr.mqtt.model.session; 2 | 3 | import javasabr.mqtt.model.message.MqttMessageType; 4 | import javasabr.mqtt.model.reason.code.ReasonCode; 5 | import org.jspecify.annotations.Nullable; 6 | 7 | public interface TrackedMessageMeta { 8 | 9 | MqttMessageType messageType(); 10 | 11 | @Nullable 12 | ReasonCode reasonCode(); 13 | } 14 | -------------------------------------------------------------------------------- /model/src/main/java/javasabr/mqtt/model/data/type/StringPair.java: -------------------------------------------------------------------------------- 1 | package javasabr.mqtt.model.data.type; 2 | 3 | import javasabr.mqtt.base.util.DebugUtils; 4 | 5 | public record StringPair(String name, String value) { 6 | 7 | static { 8 | DebugUtils.registerIncludedFields("name", "value"); 9 | } 10 | 11 | @Override 12 | public String toString() { 13 | return DebugUtils.toJsonString(this); 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /network/src/main/java/javasabr/mqtt/network/session/NetworkMqttSession.java: -------------------------------------------------------------------------------- 1 | package javasabr.mqtt.network.session; 2 | 3 | import javasabr.mqtt.model.session.MqttSession; 4 | import javasabr.mqtt.network.user.NetworkMqttUser; 5 | 6 | public interface NetworkMqttSession extends MqttSession { 7 | 8 | /** 9 | * @return the count of resent publishes 10 | */ 11 | int resendNotConfirmedPublishesTo(NetworkMqttUser user); 12 | } 13 | -------------------------------------------------------------------------------- /core-service/src/main/java/javasabr/mqtt/service/publish/handler/MqttPublishInMessageHandler.java: -------------------------------------------------------------------------------- 1 | package javasabr.mqtt.service.publish.handler; 2 | 3 | import javasabr.mqtt.model.QoS; 4 | import javasabr.mqtt.model.publishing.Publish; 5 | import javasabr.mqtt.network.user.NetworkMqttUser; 6 | 7 | public interface MqttPublishInMessageHandler { 8 | 9 | QoS qos(); 10 | 11 | void handle(NetworkMqttUser user, Publish publish); 12 | } 13 | -------------------------------------------------------------------------------- /model/src/main/java/javasabr/mqtt/model/exception/InconsistentSubscriptionStateException.java: -------------------------------------------------------------------------------- 1 | package javasabr.mqtt.model.exception; 2 | 3 | public class InconsistentSubscriptionStateException extends RuntimeException { 4 | 5 | public InconsistentSubscriptionStateException(String message) { 6 | super(message); 7 | } 8 | 9 | public InconsistentSubscriptionStateException(Throwable cause) { 10 | super(cause); 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /acl-engine/README.md: -------------------------------------------------------------------------------- 1 | ## Summary 2 | 3 | This module implements an Access Control List rules engine for the MQTT broker. 4 | **_ACL Rules Engine_** stores rules parsed by **_ACL Rules Loader_** and verify user authorization requests against the rules. 5 | It utilizes order-based priority: once a rule matches, its permission (allow or deny) is applied and subsequent rules 6 | are skipped. By default, it denies any incoming request unless it's allowed explicitly. 7 | -------------------------------------------------------------------------------- /core-service/src/main/java/javasabr/mqtt/service/publish/handler/MqttPublishOutMessageHandler.java: -------------------------------------------------------------------------------- 1 | package javasabr.mqtt.service.publish.handler; 2 | 3 | import javasabr.mqtt.model.QoS; 4 | import javasabr.mqtt.model.publishing.Publish; 5 | import javasabr.mqtt.model.subscriber.SingleSubscriber; 6 | 7 | public interface MqttPublishOutMessageHandler { 8 | 9 | QoS qos(); 10 | 11 | void handle(Publish publish, SingleSubscriber subscriber); 12 | } 13 | -------------------------------------------------------------------------------- /model/src/main/java/javasabr/mqtt/model/session/TrackableMessageCallback.java: -------------------------------------------------------------------------------- 1 | package javasabr.mqtt.model.session; 2 | 3 | import javasabr.mqtt.model.MqttUser; 4 | import javasabr.mqtt.model.message.TrackableMqttMessage; 5 | 6 | public interface TrackableMessageCallback { 7 | 8 | /** 9 | * @return true if this handler should be de-register 10 | */ 11 | boolean accept(MqttUser user, MqttSession session, TrackableMqttMessage message); 12 | } 13 | -------------------------------------------------------------------------------- /test-support/build.gradle: -------------------------------------------------------------------------------- 1 | plugins { 2 | id("java-library") 3 | id("groovy") 4 | } 5 | 6 | description = "Provide all nesessary dependencies for writing tests" 7 | 8 | dependencies { 9 | api libs.rlib.logger.impl 10 | api libs.hivemq.mqtt.client 11 | api libs.moquette.broker 12 | api libs.spring.test 13 | api libs.spock.core 14 | api libs.spock.spring 15 | api libs.groovy.all 16 | api libs.byte.buddy.dep 17 | api libs.objenesis 18 | } -------------------------------------------------------------------------------- /core-service/src/main/java/javasabr/mqtt/service/AuthorizationService.java: -------------------------------------------------------------------------------- 1 | package javasabr.mqtt.service; 2 | 3 | import javasabr.mqtt.model.MqttUser; 4 | import javasabr.mqtt.model.topic.TopicFilter; 5 | import javasabr.mqtt.model.topic.TopicName; 6 | 7 | public interface AuthorizationService { 8 | 9 | boolean authorizePublish(MqttUser user, TopicName topicName); 10 | 11 | boolean authorizeSubscribe(MqttUser user, TopicFilter topicFilter); 12 | } 13 | -------------------------------------------------------------------------------- /embedded/build.gradle: -------------------------------------------------------------------------------- 1 | plugins { 2 | id("java-library") 3 | id("configure-java") 4 | } 5 | 6 | description = "The module to embed MQTT Broker as a part to another application" 7 | 8 | dependencies { 9 | implementation projects.coreService 10 | implementation libs.rlib.logger.slf4j 11 | 12 | testImplementation projects.testSupport 13 | } 14 | 15 | tasks.withType(GroovyCompile).configureEach { 16 | options.forkOptions.jvmArgs += "--enable-preview" 17 | } 18 | -------------------------------------------------------------------------------- /network/build.gradle: -------------------------------------------------------------------------------- 1 | plugins { 2 | id("java-library") 3 | id("configure-java") 4 | id("groovy") 5 | } 6 | 7 | description = "Implementation of network layer for MQTT Protocol" 8 | 9 | dependencies { 10 | api projects.model 11 | api libs.project.reactor.core 12 | api libs.rlib.network 13 | api libs.rlib.logger.api 14 | 15 | testImplementation projects.testSupport 16 | testImplementation libs.rlib.logger.impl 17 | testFixturesApi projects.testSupport 18 | } 19 | -------------------------------------------------------------------------------- /acl-engine/src/main/java/javasabr/mqtt/acl/engine/model/rule/Rule.java: -------------------------------------------------------------------------------- 1 | package javasabr.mqtt.acl.engine.model.rule; 2 | 3 | import javasabr.mqtt.acl.engine.model.Action; 4 | import javasabr.mqtt.model.MqttUser; 5 | import javasabr.mqtt.model.acl.Operation; 6 | import javasabr.mqtt.model.topic.AbstractTopic; 7 | 8 | public interface Rule { 9 | 10 | Operation operation(); 11 | 12 | Action action(); 13 | 14 | boolean test(MqttUser mqttUser, Operation operation, AbstractTopic topic); 15 | } 16 | -------------------------------------------------------------------------------- /application/src/main/resources/log4j2.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /core-service/src/main/java/javasabr/mqtt/service/MessageOutFactoryService.java: -------------------------------------------------------------------------------- 1 | package javasabr.mqtt.service; 2 | 3 | import javasabr.mqtt.network.MqttConnection; 4 | import javasabr.mqtt.network.user.NetworkMqttUser; 5 | import javasabr.mqtt.service.message.out.factory.MqttMessageOutFactory; 6 | 7 | public interface MessageOutFactoryService { 8 | 9 | MqttMessageOutFactory resolveFactory(NetworkMqttUser user); 10 | 11 | MqttMessageOutFactory resolveFactory(MqttConnection connection); 12 | } 13 | -------------------------------------------------------------------------------- /core-service/src/main/java/javasabr/mqtt/service/session/MqttSessionService.java: -------------------------------------------------------------------------------- 1 | package javasabr.mqtt.service.session; 2 | 3 | import javasabr.mqtt.network.session.NetworkMqttSession; 4 | import reactor.core.publisher.Mono; 5 | 6 | public interface MqttSessionService { 7 | 8 | Mono restore(String clientId); 9 | 10 | Mono create(String clientId); 11 | 12 | Mono store(String clientId, NetworkMqttSession session, long expiryInterval); 13 | } 14 | -------------------------------------------------------------------------------- /model/src/main/java/javasabr/mqtt/model/acl/Operation.java: -------------------------------------------------------------------------------- 1 | package javasabr.mqtt.model.acl; 2 | 3 | import java.util.function.Consumer; 4 | import javasabr.rlib.collections.array.Array; 5 | import org.jspecify.annotations.NonNull; 6 | 7 | public enum Operation { 8 | PUBLISH, 9 | SUBSCRIBE; 10 | 11 | private static final Array<@NonNull Operation> CACHE = Array.of(values()); 12 | 13 | public static void forEach(Consumer action) { 14 | CACHE.forEach(action); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /acl-engine/src/main/java/javasabr/mqtt/acl/engine/model/condition/ClientIdCondition.java: -------------------------------------------------------------------------------- 1 | package javasabr.mqtt.acl.engine.model.condition; 2 | 3 | import javasabr.mqtt.acl.engine.model.matcher.ValueMatcher; 4 | import javasabr.mqtt.model.MqttUser; 5 | 6 | public record ClientIdCondition(ValueMatcher expectedClientId) implements MqttUserCondition { 7 | 8 | @Override 9 | public boolean test(MqttUser requestedUser) { 10 | return expectedClientId.test(requestedUser.clientId()); 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /acl-engine/src/main/java/javasabr/mqtt/acl/engine/model/condition/UserNameCondition.java: -------------------------------------------------------------------------------- 1 | package javasabr.mqtt.acl.engine.model.condition; 2 | 3 | import javasabr.mqtt.acl.engine.model.matcher.ValueMatcher; 4 | import javasabr.mqtt.model.MqttUser; 5 | 6 | public record UserNameCondition(ValueMatcher userNameMatcher) implements MqttUserCondition { 7 | 8 | @Override 9 | public boolean test(MqttUser requestedUser) { 10 | return userNameMatcher.test(requestedUser.userName()); 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /acl-engine/src/main/java/javasabr/mqtt/acl/engine/model/condition/IpAddressCondition.java: -------------------------------------------------------------------------------- 1 | package javasabr.mqtt.acl.engine.model.condition; 2 | 3 | import javasabr.mqtt.acl.engine.model.matcher.ValueMatcher; 4 | import javasabr.mqtt.model.MqttUser; 5 | 6 | public record IpAddressCondition(ValueMatcher expectedIpAddress) implements MqttUserCondition { 7 | 8 | @Override 9 | public boolean test(MqttUser requestedUser) { 10 | return expectedIpAddress.test(requestedUser.ipAddress()); 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /acl-engine/src/main/java/javasabr/mqtt/acl/engine/model/matcher/TopicNameMatcher.java: -------------------------------------------------------------------------------- 1 | package javasabr.mqtt.acl.engine.model.matcher; 2 | 3 | import java.util.Objects; 4 | import javasabr.mqtt.model.topic.AbstractTopic; 5 | import javasabr.mqtt.model.topic.TopicName; 6 | 7 | public record TopicNameMatcher(TopicName expectedTopic) implements ValueMatcher { 8 | 9 | @Override 10 | public boolean test(AbstractTopic requestedTopic) { 11 | return Objects.equals(expectedTopic.rawTopic(), requestedTopic.rawTopic()); 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /core-service/src/main/java/javasabr/mqtt/service/TopicService.java: -------------------------------------------------------------------------------- 1 | package javasabr.mqtt.service; 2 | 3 | import javasabr.mqtt.model.topic.TopicFilter; 4 | import javasabr.mqtt.model.topic.TopicName; 5 | import javasabr.mqtt.network.user.NetworkMqttUser; 6 | 7 | public interface TopicService { 8 | 9 | TopicFilter createTopicFilter(NetworkMqttUser user, String rawTopicFilter); 10 | 11 | boolean isValidTopicFilter(NetworkMqttUser user, String rawTopicFilter); 12 | 13 | TopicName createTopicName(NetworkMqttUser user, String rawTopicName); 14 | } 15 | -------------------------------------------------------------------------------- /model/src/main/java/javasabr/mqtt/model/subscriber/Subscriber.java: -------------------------------------------------------------------------------- 1 | package javasabr.mqtt.model.subscriber; 2 | 3 | import javasabr.mqtt.model.MqttUser; 4 | 5 | public sealed interface Subscriber permits SingleSubscriber, SharedSubscriber { 6 | 7 | /** 8 | * Resolves the owner of a subscription to send a publishing. 9 | */ 10 | default MqttUser resolveUser() { 11 | return resolveSingle().user(); 12 | } 13 | 14 | /** 15 | * Resolves the owner of a subscription to send a publishing. 16 | */ 17 | SingleSubscriber resolveSingle(); 18 | } 19 | -------------------------------------------------------------------------------- /core-service/src/main/java/javasabr/mqtt/service/message/validator/MqttInMessageFieldValidator.java: -------------------------------------------------------------------------------- 1 | package javasabr.mqtt.service.message.validator; 2 | 3 | import javasabr.mqtt.model.MqttUser; 4 | import javasabr.mqtt.network.MqttConnection; 5 | import javasabr.mqtt.network.message.in.MqttInMessage; 6 | 7 | public abstract class MqttInMessageFieldValidator { 8 | 9 | public abstract boolean isNotValid(MqttConnection connection, U user, M message); 10 | 11 | public int order() { 12 | return 0; 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /model/src/main/java/javasabr/mqtt/model/session/ActiveSubscriptions.java: -------------------------------------------------------------------------------- 1 | package javasabr.mqtt.model.session; 2 | 3 | import javasabr.mqtt.model.subscription.Subscription; 4 | import javasabr.mqtt.model.topic.TopicFilter; 5 | import javasabr.rlib.collections.array.Array; 6 | 7 | public interface ActiveSubscriptions { 8 | 9 | void add(Subscription subscription); 10 | 11 | void remove(Subscription subscription); 12 | 13 | void removeByTopicFilter(TopicFilter topicFilter); 14 | 15 | Array subscriptions(); 16 | 17 | Array findBySubscriptionId(int subscriptionId); 18 | } 19 | -------------------------------------------------------------------------------- /application/src/main/java/javasabr/mqtt/broker/application/MqttBrokerApplication.java: -------------------------------------------------------------------------------- 1 | package javasabr.mqtt.broker.application; 2 | 3 | import javasabr.mqtt.broker.application.config.MqttBrokerSpringConfig; 4 | import lombok.RequiredArgsConstructor; 5 | import org.springframework.boot.SpringApplication; 6 | import org.springframework.context.annotation.Import; 7 | 8 | @Import({ 9 | MqttBrokerSpringConfig.class 10 | }) 11 | @RequiredArgsConstructor 12 | public class MqttBrokerApplication { 13 | static void main(String[] args) { 14 | SpringApplication.run(MqttBrokerApplication.class, args); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /network/src/main/java/javasabr/mqtt/network/impl/ExternalNetworkMqttUser.java: -------------------------------------------------------------------------------- 1 | package javasabr.mqtt.network.impl; 2 | 3 | import javasabr.mqtt.base.util.DebugUtils; 4 | import javasabr.mqtt.network.MqttConnection; 5 | import javasabr.mqtt.network.handler.NetworkMqttUserReleaseHandler; 6 | 7 | public class ExternalNetworkMqttUser extends AbstractNetworkMqttUser { 8 | 9 | static { 10 | DebugUtils.registerIncludedFields("clientId"); 11 | } 12 | 13 | public ExternalNetworkMqttUser(MqttConnection connection, NetworkMqttUserReleaseHandler releaseHandler) { 14 | super(connection, releaseHandler); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /network/src/main/java/javasabr/mqtt/network/impl/InternalNetworkMqttUser.java: -------------------------------------------------------------------------------- 1 | package javasabr.mqtt.network.impl; 2 | 3 | import javasabr.mqtt.base.util.DebugUtils; 4 | import javasabr.mqtt.network.MqttConnection; 5 | import javasabr.mqtt.network.handler.NetworkMqttUserReleaseHandler; 6 | 7 | public class InternalNetworkMqttUser extends AbstractNetworkMqttUser { 8 | 9 | static { 10 | DebugUtils.registerIncludedFields("clientId"); 11 | } 12 | 13 | public InternalNetworkMqttUser(MqttConnection connection, NetworkMqttUserReleaseHandler releaseHandler) { 14 | super(connection, releaseHandler); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /network/src/main/java/javasabr/mqtt/network/message/out/PingRequestMqtt311OutMessage.java: -------------------------------------------------------------------------------- 1 | package javasabr.mqtt.network.message.out; 2 | 3 | import javasabr.mqtt.model.message.MqttMessageType; 4 | 5 | /** 6 | * PING request. 7 | */ 8 | public class PingRequestMqtt311OutMessage extends MqttOutMessage { 9 | 10 | private static final byte MESSAGE_TYPE = (byte) MqttMessageType.PING_REQUEST.ordinal(); 11 | 12 | @Override 13 | protected byte messageTypeId() { 14 | return MESSAGE_TYPE; 15 | } 16 | 17 | @Override 18 | public MqttMessageType messageType() { 19 | return MqttMessageType.PING_REQUEST; 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /network/src/main/java/javasabr/mqtt/network/user/ConfigurableNetworkMqttUser.java: -------------------------------------------------------------------------------- 1 | package javasabr.mqtt.network.user; 2 | 3 | import javasabr.mqtt.network.message.out.ConnectAckMqtt311OutMessage; 4 | import javasabr.mqtt.network.session.NetworkMqttSession; 5 | import org.jspecify.annotations.Nullable; 6 | import reactor.core.publisher.Mono; 7 | 8 | public interface ConfigurableNetworkMqttUser extends NetworkMqttUser { 9 | 10 | void clientId(String clientId); 11 | 12 | void session(@Nullable NetworkMqttSession session); 13 | 14 | void reject(ConnectAckMqtt311OutMessage connectAsk); 15 | 16 | Mono release(); 17 | } 18 | -------------------------------------------------------------------------------- /network/src/main/java/javasabr/mqtt/network/message/out/PingResponseMqtt311OutMessage.java: -------------------------------------------------------------------------------- 1 | package javasabr.mqtt.network.message.out; 2 | 3 | import javasabr.mqtt.model.message.MqttMessageType; 4 | 5 | /** 6 | * PING response. 7 | */ 8 | public class PingResponseMqtt311OutMessage extends MqttOutMessage { 9 | 10 | private static final byte MESSAGE_TYPE = (byte) MqttMessageType.PING_RESPONSE.ordinal(); 11 | 12 | @Override 13 | protected byte messageTypeId() { 14 | return MESSAGE_TYPE; 15 | } 16 | 17 | @Override 18 | public MqttMessageType messageType() { 19 | return MqttMessageType.PING_RESPONSE; 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /acl-groovy-dsl/src/main/groovy/javasabr/mqtt/acl/groovy/dsl/builder/DenyPublishRuleBuilder.groovy: -------------------------------------------------------------------------------- 1 | //file:noinspection unused 2 | package javasabr.mqtt.acl.groovy.dsl.builder 3 | 4 | import javasabr.mqtt.acl.engine.model.Action 5 | import javasabr.mqtt.acl.engine.model.condition.TopicCondition 6 | import javasabr.mqtt.acl.engine.model.rule.DenyPublishRule 7 | import javasabr.mqtt.acl.engine.model.rule.Rule 8 | 9 | class DenyPublishRuleBuilder extends PublishRuleBuilder { 10 | 11 | DenyPublishRuleBuilder() { super(Action.DENY) } 12 | 13 | Rule build() { 14 | return new DenyPublishRule(userCondition, new TopicCondition(topicNames)) 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /model/src/main/java/javasabr/mqtt/model/session/MqttSession.java: -------------------------------------------------------------------------------- 1 | package javasabr.mqtt.model.session; 2 | 3 | public interface MqttSession { 4 | 5 | String clientId(); 6 | 7 | int generateMessageId(); 8 | 9 | /** 10 | * @return the expiration time in ms or -1 if it should not be expired now. 11 | */ 12 | long expirationTime(); 13 | 14 | MessageTacker inMessageTracker(); 15 | MessageTacker outMessageTracker(); 16 | 17 | ProcessingPublishes inProcessingPublishes(); 18 | ProcessingPublishes outProcessingPublishes(); 19 | 20 | ActiveSubscriptions activeSubscriptions(); 21 | TopicNameMapping topicNameMapping(); 22 | 23 | } 24 | -------------------------------------------------------------------------------- /acl-groovy-dsl/src/main/groovy/javasabr/mqtt/acl/groovy/dsl/builder/AllowPublishRuleBuilder.groovy: -------------------------------------------------------------------------------- 1 | //file:noinspection unused 2 | package javasabr.mqtt.acl.groovy.dsl.builder 3 | 4 | import javasabr.mqtt.acl.engine.model.Action 5 | import javasabr.mqtt.acl.engine.model.condition.TopicCondition 6 | import javasabr.mqtt.acl.engine.model.rule.AllowPublishRule 7 | import javasabr.mqtt.acl.engine.model.rule.Rule 8 | 9 | class AllowPublishRuleBuilder extends PublishRuleBuilder { 10 | 11 | AllowPublishRuleBuilder() { super(Action.ALLOW) } 12 | 13 | Rule build() { 14 | return new AllowPublishRule(userCondition, new TopicCondition(topicNames)) 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /acl-groovy-dsl/src/main/groovy/javasabr/mqtt/acl/groovy/dsl/builder/DenySubscribeRuleBuilder.groovy: -------------------------------------------------------------------------------- 1 | //file:noinspection unused 2 | package javasabr.mqtt.acl.groovy.dsl.builder 3 | 4 | import javasabr.mqtt.acl.engine.model.Action 5 | import javasabr.mqtt.acl.engine.model.condition.TopicCondition 6 | import javasabr.mqtt.acl.engine.model.rule.DenySubscribeRule 7 | import javasabr.mqtt.acl.engine.model.rule.Rule 8 | 9 | class DenySubscribeRuleBuilder extends SubscribeRuleBuilder { 10 | 11 | DenySubscribeRuleBuilder() { super(Action.DENY) } 12 | 13 | Rule build() { 14 | return new DenySubscribeRule(userCondition, new TopicCondition(topicFilters)) 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /core-service/src/main/java/javasabr/mqtt/service/impl/DisabledAuthorizationService.java: -------------------------------------------------------------------------------- 1 | package javasabr.mqtt.service.impl; 2 | 3 | import javasabr.mqtt.model.MqttUser; 4 | import javasabr.mqtt.model.topic.TopicFilter; 5 | import javasabr.mqtt.model.topic.TopicName; 6 | import javasabr.mqtt.service.AuthorizationService; 7 | 8 | public class DisabledAuthorizationService implements AuthorizationService { 9 | @Override 10 | public boolean authorizePublish(MqttUser user, TopicName topicName) { 11 | return true; 12 | } 13 | 14 | @Override 15 | public boolean authorizeSubscribe(MqttUser user, TopicFilter topicFilter) { 16 | return true; 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /acl-groovy-dsl/src/main/groovy/javasabr/mqtt/acl/groovy/dsl/builder/AllowSubscribeRuleBuilder.groovy: -------------------------------------------------------------------------------- 1 | //file:noinspection unused 2 | package javasabr.mqtt.acl.groovy.dsl.builder 3 | 4 | import javasabr.mqtt.acl.engine.model.Action 5 | import javasabr.mqtt.acl.engine.model.condition.TopicCondition 6 | import javasabr.mqtt.acl.engine.model.rule.AllowSubscribeRule 7 | import javasabr.mqtt.acl.engine.model.rule.Rule 8 | 9 | class AllowSubscribeRuleBuilder extends SubscribeRuleBuilder { 10 | 11 | AllowSubscribeRuleBuilder() { super(Action.ALLOW) } 12 | 13 | Rule build() { 14 | return new AllowSubscribeRule(userCondition, new TopicCondition(topicFilters)) 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /core-service/src/main/java/javasabr/mqtt/service/message/handler/MqttInMessageHandler.java: -------------------------------------------------------------------------------- 1 | package javasabr.mqtt.service.message.handler; 2 | 3 | import javasabr.mqtt.model.message.MqttMessageType; 4 | import javasabr.mqtt.network.MqttConnection; 5 | import javasabr.mqtt.network.message.in.MqttInMessage; 6 | import javasabr.mqtt.network.user.NetworkMqttUser; 7 | 8 | public interface MqttInMessageHandler { 9 | 10 | MqttMessageType messageType(); 11 | 12 | Class expectedUserType(); 13 | 14 | void processValidMessage(MqttConnection connection, MqttInMessage mqttInMessage); 15 | 16 | void processInvalidMessage(MqttConnection connection, MqttInMessage mqttInMessage); 17 | } 18 | -------------------------------------------------------------------------------- /network/src/main/java/javasabr/mqtt/network/message/in/PingRequestMqttInMessage.java: -------------------------------------------------------------------------------- 1 | package javasabr.mqtt.network.message.in; 2 | 3 | import javasabr.mqtt.model.message.MqttMessageType; 4 | 5 | /** 6 | * PING request. 7 | */ 8 | public class PingRequestMqttInMessage extends MqttInMessage { 9 | 10 | public static final byte MESSAGE_TYPE = (byte) MqttMessageType.PING_REQUEST.ordinal(); 11 | 12 | public PingRequestMqttInMessage(byte messageFlags) { 13 | super(messageFlags); 14 | } 15 | 16 | @Override 17 | public byte messageTypeId() { 18 | return MESSAGE_TYPE; 19 | } 20 | 21 | @Override 22 | public MqttMessageType messageType() { 23 | return MqttMessageType.PING_REQUEST; 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /acl-engine/src/main/java/javasabr/mqtt/acl/engine/model/condition/AllOfCondition.java: -------------------------------------------------------------------------------- 1 | package javasabr.mqtt.acl.engine.model.condition; 2 | 3 | import javasabr.mqtt.model.MqttUser; 4 | import javasabr.rlib.collections.array.Array; 5 | 6 | public record AllOfCondition(Array expectedUsers) implements MqttUserCondition { 7 | 8 | public AllOfCondition(MqttUserCondition... expectedUsers) { 9 | this(Array.of(expectedUsers)); 10 | } 11 | 12 | @Override 13 | public boolean test(MqttUser requestedUser) { 14 | for (MqttUserCondition condition : expectedUsers) { 15 | if (!condition.test(requestedUser)) { 16 | return false; 17 | } 18 | } 19 | return true; 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /acl-engine/src/main/java/javasabr/mqtt/acl/engine/model/condition/AnyOfCondition.java: -------------------------------------------------------------------------------- 1 | package javasabr.mqtt.acl.engine.model.condition; 2 | 3 | import javasabr.mqtt.model.MqttUser; 4 | import javasabr.rlib.collections.array.Array; 5 | 6 | public record AnyOfCondition(Array expectedUsers) implements MqttUserCondition { 7 | 8 | public AnyOfCondition(MqttUserCondition... expectedUsers) { 9 | this(Array.of(expectedUsers)); 10 | } 11 | 12 | @Override 13 | public boolean test(MqttUser requestedUser) { 14 | for (MqttUserCondition condition : expectedUsers) { 15 | if (condition.test(requestedUser)) { 16 | return true; 17 | } 18 | } 19 | return false; 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /network/src/main/java/javasabr/mqtt/network/message/in/PingResponseMqttInMessage.java: -------------------------------------------------------------------------------- 1 | package javasabr.mqtt.network.message.in; 2 | 3 | import javasabr.mqtt.model.message.MqttMessageType; 4 | 5 | /** 6 | * PING response. 7 | */ 8 | public class PingResponseMqttInMessage extends MqttInMessage { 9 | 10 | public static final byte MESSAGE_TYPE = (byte) MqttMessageType.PING_RESPONSE.ordinal(); 11 | 12 | public PingResponseMqttInMessage(byte messageFlags) { 13 | super(messageFlags); 14 | } 15 | 16 | @Override 17 | public byte messageTypeId() { 18 | return MESSAGE_TYPE; 19 | } 20 | 21 | @Override 22 | public MqttMessageType messageType() { 23 | return MqttMessageType.PING_RESPONSE; 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /model/src/main/java/javasabr/mqtt/model/session/ProcessingPublishes.java: -------------------------------------------------------------------------------- 1 | package javasabr.mqtt.model.session; 2 | 3 | import javasabr.mqtt.model.MqttUser; 4 | import javasabr.mqtt.model.message.TrackableMqttMessage; 5 | import javasabr.mqtt.model.publishing.Publish; 6 | 7 | public interface ProcessingPublishes { 8 | 9 | void register(Publish publish, TrackableMessageCallback callback, PublishRetryer retryer); 10 | 11 | /** 12 | * @return true if was found some callback for this message 13 | */ 14 | boolean apply(MqttUser user, TrackableMqttMessage message); 15 | 16 | /** 17 | * @return true if was found some callback for this message 18 | */ 19 | boolean remove(TrackableMqttMessage message); 20 | 21 | int size(); 22 | } 23 | -------------------------------------------------------------------------------- /acl-engine/src/main/java/javasabr/mqtt/acl/engine/builder/ClientMatcherBuilder.java: -------------------------------------------------------------------------------- 1 | //file:noinspection unused 2 | package javasabr.mqtt.acl.engine.builder; 3 | 4 | import java.util.regex.Pattern; 5 | import javasabr.mqtt.acl.engine.model.matcher.EqualsMatcher; 6 | import javasabr.mqtt.acl.engine.model.matcher.RegexMatcher; 7 | import javasabr.mqtt.acl.engine.model.matcher.ValueMatcher; 8 | 9 | public interface ClientMatcherBuilder { 10 | 11 | default ValueMatcher eq(String string) { 12 | return new EqualsMatcher(string); 13 | } 14 | 15 | default ValueMatcher regex(String string) { 16 | return new RegexMatcher(Pattern.compile(string)); 17 | } 18 | 19 | default ValueMatcher anyone() { 20 | return ValueMatcher.MATCH_ANY; 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /model/src/main/java/javasabr/mqtt/model/exception/ConnectionRejectException.java: -------------------------------------------------------------------------------- 1 | package javasabr.mqtt.model.exception; 2 | 3 | import javasabr.mqtt.model.reason.code.ConnectAckReasonCode; 4 | import lombok.AccessLevel; 5 | import lombok.Getter; 6 | import lombok.experimental.FieldDefaults; 7 | 8 | @Getter 9 | @FieldDefaults(level = AccessLevel.PRIVATE, makeFinal = true) 10 | public class ConnectionRejectException extends MqttException { 11 | 12 | ConnectAckReasonCode reasonCode; 13 | 14 | public ConnectionRejectException(ConnectAckReasonCode reasonCode) { 15 | this.reasonCode = reasonCode; 16 | } 17 | 18 | public ConnectionRejectException(Throwable cause, ConnectAckReasonCode reasonCode) { 19 | super(cause); 20 | this.reasonCode = reasonCode; 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /core-service/src/main/java/javasabr/mqtt/service/message/handler/impl/PublishAckMqttInMessageHandler.java: -------------------------------------------------------------------------------- 1 | package javasabr.mqtt.service.message.handler.impl; 2 | 3 | import javasabr.mqtt.model.message.MqttMessageType; 4 | import javasabr.mqtt.network.message.in.PublishAckMqttInMessage; 5 | import javasabr.mqtt.service.MessageOutFactoryService; 6 | 7 | public class PublishAckMqttInMessageHandler extends 8 | ProcessingOutPublishesMqttInMessageHandler { 9 | 10 | public PublishAckMqttInMessageHandler(MessageOutFactoryService messageOutFactoryService) { 11 | super(PublishAckMqttInMessage.class, messageOutFactoryService); 12 | } 13 | 14 | @Override 15 | public MqttMessageType messageType() { 16 | return MqttMessageType.PUBLISH_ACK; 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /model/src/main/java/javasabr/mqtt/model/subscriber/SingleSubscriber.java: -------------------------------------------------------------------------------- 1 | package javasabr.mqtt.model.subscriber; 2 | 3 | import com.fasterxml.jackson.annotation.JsonValue; 4 | import javasabr.mqtt.model.MqttUser; 5 | import javasabr.mqtt.model.QoS; 6 | import javasabr.mqtt.model.subscription.Subscription; 7 | 8 | public record SingleSubscriber(MqttUser user, Subscription subscription) implements Subscriber { 9 | 10 | @Override 11 | public SingleSubscriber resolveSingle() { 12 | return this; 13 | } 14 | 15 | public QoS qos() { 16 | return subscription.qos(); 17 | } 18 | 19 | @JsonValue 20 | @Override 21 | public String toString() { 22 | return "[" + user + "]->[" + subscription.topicFilter().rawTopic() + "|" + subscription.qos().level() + "]"; 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /model/src/main/java/javasabr/mqtt/model/MqttUser.java: -------------------------------------------------------------------------------- 1 | package javasabr.mqtt.model; 2 | 3 | import java.util.concurrent.CompletionStage; 4 | import javasabr.mqtt.model.message.SendableMqttMessage; 5 | import javasabr.mqtt.model.session.MqttSession; 6 | import org.jspecify.annotations.Nullable; 7 | 8 | public interface MqttUser { 9 | 10 | String clientId(); 11 | 12 | @Nullable 13 | String userName(); 14 | 15 | String ipAddress(); 16 | 17 | @Nullable 18 | MqttSession session(); 19 | 20 | MqttClientConnectionConfig connectionConfig(); 21 | 22 | void sendInBackground(SendableMqttMessage message); 23 | 24 | /** 25 | * @return the feature with result of delivering the message 26 | */ 27 | CompletionStage sendAsync(SendableMqttMessage message); 28 | } 29 | -------------------------------------------------------------------------------- /network/src/main/java/javasabr/mqtt/network/message/out/DisconnectMqtt311OutMessage.java: -------------------------------------------------------------------------------- 1 | package javasabr.mqtt.network.message.out; 2 | 3 | import javasabr.mqtt.model.message.MqttMessageType; 4 | import javasabr.mqtt.network.MqttConnection; 5 | 6 | /** 7 | * Disconnect notification. 8 | */ 9 | public class DisconnectMqtt311OutMessage extends MqttOutMessage { 10 | 11 | private static final byte MESSAGE_TYPE = (byte) MqttMessageType.DISCONNECT.ordinal(); 12 | 13 | @Override 14 | public int expectedLength(MqttConnection connection) { 15 | return 0; 16 | } 17 | 18 | @Override 19 | protected byte messageTypeId() { 20 | return MESSAGE_TYPE; 21 | } 22 | 23 | @Override 24 | public MqttMessageType messageType() { 25 | return MqttMessageType.DISCONNECT; 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /core-service/src/main/java/javasabr/mqtt/service/handler/client/ExternalNetworkMqttUserReleaseHandler.java: -------------------------------------------------------------------------------- 1 | package javasabr.mqtt.service.handler.client; 2 | 3 | import javasabr.mqtt.network.impl.ExternalNetworkMqttUser; 4 | import javasabr.mqtt.service.ClientIdRegistry; 5 | import javasabr.mqtt.service.SubscriptionService; 6 | import javasabr.mqtt.service.session.MqttSessionService; 7 | 8 | public class ExternalNetworkMqttUserReleaseHandler extends 9 | AbstractNetworkMqttUserReleaseHandler { 10 | 11 | public ExternalNetworkMqttUserReleaseHandler( 12 | ClientIdRegistry clientIdRegistry, 13 | MqttSessionService sessionService, 14 | SubscriptionService subscriptionService) { 15 | super(clientIdRegistry, sessionService, subscriptionService); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /model/src/main/java/javasabr/mqtt/model/topic/TopicName.java: -------------------------------------------------------------------------------- 1 | package javasabr.mqtt.model.topic; 2 | 3 | public class TopicName extends AbstractTopic { 4 | 5 | public static final TopicName INVALID_TOPIC_NAME = new TopicName("$invalid$") { 6 | @Override 7 | public boolean isInvalid() { 8 | return true; 9 | } 10 | }; 11 | 12 | public static final TopicName EMPTY_TOPIC_NAME = new TopicName("") { 13 | @Override 14 | public boolean isEmpty() { 15 | return true; 16 | } 17 | }; 18 | 19 | public TopicName(String topicName) { 20 | super(topicName); 21 | } 22 | 23 | public boolean isEmpty() { 24 | return false; 25 | } 26 | 27 | public static TopicName valueOf(String rawTopicName) { 28 | return new TopicName(rawTopicName); 29 | } 30 | } 31 | 32 | -------------------------------------------------------------------------------- /core-service/src/test/groovy/javasabr/mqtt/service/publish/handler/impl/QosMqttPublishInMessageHandlerTest.groovy: -------------------------------------------------------------------------------- 1 | package javasabr.mqtt.service.publish.handler.impl 2 | 3 | import javasabr.mqtt.service.IntegrationServiceSpecification 4 | 5 | abstract class QosMqttPublishInMessageHandlerTest extends IntegrationServiceSpecification { 6 | static { 7 | //LoggerManager.enable(AbstractMqttPublishInMessageHandler.class, LoggerLevel.DEBUG) 8 | //LoggerManager.enable(TrackableMqttPublishInMessageHandler.class, LoggerLevel.DEBUG) 9 | //LoggerManager.enable(Qos0MqttPublishInMessageHandler.class, LoggerLevel.DEBUG) 10 | //LoggerManager.enable(Qos1MqttPublishInMessageHandler.class, LoggerLevel.DEBUG) 11 | //LoggerManager.enable(Qos2MqttPublishInMessageHandler.class, LoggerLevel.DEBUG) 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /model/src/main/java/javasabr/mqtt/model/PayloadFormat.java: -------------------------------------------------------------------------------- 1 | package javasabr.mqtt.model; 2 | 3 | import lombok.AccessLevel; 4 | import lombok.Getter; 5 | import lombok.RequiredArgsConstructor; 6 | import lombok.experimental.Accessors; 7 | import lombok.experimental.FieldDefaults; 8 | 9 | @Getter 10 | @RequiredArgsConstructor 11 | @Accessors(fluent = true) 12 | @FieldDefaults(level = AccessLevel.PRIVATE, makeFinal = true) 13 | public enum PayloadFormat { 14 | BINARY(0), 15 | UTF8_STRING(1), 16 | INVALID(2), 17 | UNDEFINED(3); 18 | 19 | public static PayloadFormat fromCode(long code) { 20 | if (BINARY.code == code) { 21 | return BINARY; 22 | } else if (UTF8_STRING.code == code) { 23 | return UTF8_STRING; 24 | } 25 | return UNDEFINED; 26 | } 27 | 28 | int code; 29 | } 30 | -------------------------------------------------------------------------------- /model/src/main/java/javasabr/mqtt/model/session/MessageTacker.java: -------------------------------------------------------------------------------- 1 | package javasabr.mqtt.model.session; 2 | 3 | import javasabr.mqtt.model.message.MqttMessageType; 4 | import javasabr.mqtt.model.reason.code.ReasonCode; 5 | import org.jspecify.annotations.Nullable; 6 | 7 | public interface MessageTacker { 8 | 9 | @Nullable 10 | TrackedMessageMeta stored(int messageId); 11 | 12 | void add(int messageId, MqttMessageType messageType); 13 | 14 | void add(int messageId, MqttMessageType messageType, @Nullable ReasonCode reasonCode); 15 | 16 | /** 17 | * @return true if was added a new entry instead of updating current 18 | */ 19 | boolean update(int messageId, MqttMessageType messageType, @Nullable ReasonCode reasonCode); 20 | 21 | @Nullable 22 | TrackedMessageMeta remove(int messageId); 23 | } 24 | -------------------------------------------------------------------------------- /core-service/src/main/java/javasabr/mqtt/service/message/handler/impl/PublishReceiveMqttInMessageHandler.java: -------------------------------------------------------------------------------- 1 | package javasabr.mqtt.service.message.handler.impl; 2 | 3 | import javasabr.mqtt.model.message.MqttMessageType; 4 | import javasabr.mqtt.network.message.in.PublishReceivedMqttInMessage; 5 | import javasabr.mqtt.service.MessageOutFactoryService; 6 | 7 | public class PublishReceiveMqttInMessageHandler extends 8 | ProcessingOutPublishesMqttInMessageHandler { 9 | 10 | public PublishReceiveMqttInMessageHandler(MessageOutFactoryService messageOutFactoryService) { 11 | super(PublishReceivedMqttInMessage.class, messageOutFactoryService); 12 | } 13 | 14 | @Override 15 | public MqttMessageType messageType() { 16 | return MqttMessageType.PUBLISH_RECEIVED; 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /core-service/src/main/java/javasabr/mqtt/service/message/handler/impl/PublishCompleteMqttInMessageHandler.java: -------------------------------------------------------------------------------- 1 | package javasabr.mqtt.service.message.handler.impl; 2 | 3 | import javasabr.mqtt.model.message.MqttMessageType; 4 | import javasabr.mqtt.network.message.in.PublishCompleteMqttInMessage; 5 | import javasabr.mqtt.service.MessageOutFactoryService; 6 | 7 | public class PublishCompleteMqttInMessageHandler extends 8 | ProcessingOutPublishesMqttInMessageHandler { 9 | 10 | public PublishCompleteMqttInMessageHandler(MessageOutFactoryService messageOutFactoryService) { 11 | super(PublishCompleteMqttInMessage.class, messageOutFactoryService); 12 | } 13 | 14 | @Override 15 | public MqttMessageType messageType() { 16 | return MqttMessageType.PUBLISH_COMPLETE; 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /core-service/src/main/java/javasabr/mqtt/service/impl/SimpleAuthenticationService.java: -------------------------------------------------------------------------------- 1 | package javasabr.mqtt.service.impl; 2 | 3 | import javasabr.mqtt.service.AuthenticationService; 4 | import javasabr.mqtt.service.CredentialSource; 5 | import lombok.RequiredArgsConstructor; 6 | import reactor.core.publisher.Mono; 7 | 8 | @RequiredArgsConstructor 9 | public class SimpleAuthenticationService implements AuthenticationService { 10 | 11 | private final CredentialSource credentialSource; 12 | private final boolean allowAnonymousAuth; 13 | 14 | @Override 15 | public Mono auth(String userName, byte[] password) { 16 | if (allowAnonymousAuth && userName.isEmpty()) { 17 | return Mono.just(Boolean.TRUE); 18 | } else { 19 | return credentialSource.check(userName, password); 20 | } 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /acl-engine/src/main/java/javasabr/mqtt/acl/engine/builder/TopicMatcherBuilder.java: -------------------------------------------------------------------------------- 1 | //file:noinspection unused 2 | package javasabr.mqtt.acl.engine.builder; 3 | 4 | import javasabr.mqtt.acl.engine.model.matcher.TopicFilterMatcher; 5 | import javasabr.mqtt.acl.engine.model.matcher.TopicNameMatcher; 6 | import javasabr.mqtt.acl.engine.model.matcher.ValueMatcher; 7 | import javasabr.mqtt.model.topic.AbstractTopic; 8 | import javasabr.mqtt.model.topic.TopicFilter; 9 | import javasabr.mqtt.model.topic.TopicName; 10 | 11 | public interface TopicMatcherBuilder { 12 | 13 | default ValueMatcher eq(String string) { 14 | return new TopicNameMatcher(TopicName.valueOf(string)); 15 | } 16 | 17 | default ValueMatcher match(String string) { 18 | return new TopicFilterMatcher(TopicFilter.valueOf(string)); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /acl-groovy-dsl/src/main/groovy/javasabr/mqtt/acl/groovy/dsl/builder/AnyOfBuilder.groovy: -------------------------------------------------------------------------------- 1 | //file:noinspection unused 2 | package javasabr.mqtt.acl.groovy.dsl.builder 3 | 4 | import javasabr.mqtt.acl.engine.model.condition.AnyOfCondition 5 | import javasabr.mqtt.acl.engine.model.condition.MqttUserCondition 6 | import javasabr.rlib.collections.array.Array 7 | 8 | class AnyOfBuilder extends ConditionBuilder { 9 | 10 | ConditionBuilder allOf(Closure config) { 11 | this.conditions.add(new AllOfBuilder().buildCondition(config).build()) 12 | return this 13 | } 14 | 15 | ConditionBuilder anyOf(Closure config) { 16 | this.conditions.add(new AnyOfBuilder().buildCondition(config).build()) 17 | return this 18 | } 19 | 20 | MqttUserCondition build() { 21 | return new AnyOfCondition(Array.copyOf(conditions)) 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /model/src/main/java/javasabr/mqtt/model/SubscribeRetainHandling.java: -------------------------------------------------------------------------------- 1 | package javasabr.mqtt.model; 2 | 3 | public enum SubscribeRetainHandling { 4 | /** 5 | * Send retained messages at the time of the subscribe. 6 | */ 7 | SEND, 8 | /** 9 | * Send retained messages at subscribe only if the subscription does not currently exist. 10 | */ 11 | SEND_IF_SUBSCRIPTION_DOES_NOT_EXIST, 12 | /** 13 | * Do not send retained messages at the time of the subscribe. 14 | */ 15 | DO_NOT_SEND, 16 | INVALID; 17 | 18 | private static final SubscribeRetainHandling[] VALUES = values(); 19 | 20 | public static SubscribeRetainHandling of(int level) { 21 | if (level < 0 || level > DO_NOT_SEND.ordinal()) { 22 | return SubscribeRetainHandling.INVALID; 23 | } else { 24 | return VALUES[level]; 25 | } 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /acl-engine/src/main/java/javasabr/mqtt/acl/engine/model/condition/TopicCondition.java: -------------------------------------------------------------------------------- 1 | package javasabr.mqtt.acl.engine.model.condition; 2 | 3 | import javasabr.mqtt.acl.engine.model.matcher.AnyTopicMatcher; 4 | import javasabr.mqtt.acl.engine.model.matcher.ValueMatcher; 5 | import javasabr.mqtt.model.topic.AbstractTopic; 6 | import javasabr.rlib.collections.array.Array; 7 | 8 | public record TopicCondition(Array> expectedTopics) implements Condition { 9 | 10 | public static final TopicCondition MATCH_ANY = new TopicCondition(Array.of(new AnyTopicMatcher())); 11 | 12 | @Override 13 | public boolean test(AbstractTopic requestedTopic) { 14 | for (var topic : expectedTopics()) { 15 | if (topic.test(requestedTopic)) { 16 | return true; 17 | } 18 | } 19 | return false; 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /network/src/main/java/javasabr/mqtt/network/message/out/PublishAckMqtt311OutMessage.java: -------------------------------------------------------------------------------- 1 | package javasabr.mqtt.network.message.out; 2 | 3 | import javasabr.mqtt.model.message.MqttMessageType; 4 | import javasabr.mqtt.network.MqttConnection; 5 | 6 | /** 7 | * Publish acknowledgement. 8 | */ 9 | public class PublishAckMqtt311OutMessage extends TrackableMqttOutMessage { 10 | 11 | private static final byte MESSAGE_TYPE = (byte) MqttMessageType.PUBLISH_ACK.ordinal(); 12 | 13 | public PublishAckMqtt311OutMessage(int messageId) { 14 | super(messageId); 15 | } 16 | 17 | @Override 18 | public int expectedLength(MqttConnection connection) { 19 | return 2; 20 | } 21 | 22 | @Override 23 | protected byte messageTypeId() { 24 | return MESSAGE_TYPE; 25 | } 26 | 27 | @Override 28 | public MqttMessageType messageType() { 29 | return MqttMessageType.PUBLISH_ACK; 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /acl-groovy-dsl/src/main/groovy/javasabr/mqtt/acl/groovy/dsl/builder/PublishRuleBuilder.groovy: -------------------------------------------------------------------------------- 1 | //file:noinspection unused 2 | package javasabr.mqtt.acl.groovy.dsl.builder 3 | 4 | import javasabr.mqtt.acl.engine.model.Action 5 | import javasabr.mqtt.acl.engine.model.matcher.ValueMatcher 6 | import javasabr.mqtt.model.acl.Operation 7 | import javasabr.mqtt.model.topic.TopicName 8 | import javasabr.rlib.collections.array.ArrayFactory 9 | import javasabr.rlib.collections.array.MutableArray 10 | 11 | abstract class PublishRuleBuilder extends RuleBuilder { 12 | protected MutableArray> topicNames = ArrayFactory.mutableArray(ValueMatcher) 13 | 14 | PublishRuleBuilder(Action action) { 15 | super(action, Operation.PUBLISH) 16 | } 17 | 18 | PublishRuleBuilder topicName(ValueMatcher... topicNames) { 19 | this.topicNames.addAll(topicNames) 20 | return this 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /acl-groovy-dsl/src/main/groovy/javasabr/mqtt/acl/groovy/dsl/builder/SubscribeRuleBuilder.groovy: -------------------------------------------------------------------------------- 1 | //file:noinspection unused 2 | package javasabr.mqtt.acl.groovy.dsl.builder 3 | 4 | import javasabr.mqtt.acl.engine.model.Action 5 | import javasabr.mqtt.acl.engine.model.matcher.ValueMatcher 6 | import javasabr.mqtt.model.acl.Operation 7 | import javasabr.mqtt.model.topic.TopicFilter 8 | import javasabr.rlib.collections.array.ArrayFactory 9 | import javasabr.rlib.collections.array.MutableArray 10 | 11 | abstract class SubscribeRuleBuilder extends RuleBuilder { 12 | protected MutableArray> topicFilters = ArrayFactory.mutableArray(ValueMatcher) 13 | 14 | SubscribeRuleBuilder(Action action) { super(action, Operation.SUBSCRIBE) } 15 | 16 | SubscribeRuleBuilder topicFilter(ValueMatcher... topicFilters) { 17 | this.topicFilters.addAll(topicFilters) 18 | return this 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /network/src/main/java/javasabr/mqtt/network/message/out/TrackableMqttOutMessage.java: -------------------------------------------------------------------------------- 1 | package javasabr.mqtt.network.message.out; 2 | 3 | import java.nio.ByteBuffer; 4 | import javasabr.mqtt.base.util.DebugUtils; 5 | import javasabr.mqtt.network.MqttConnection; 6 | import lombok.AccessLevel; 7 | import lombok.Getter; 8 | import lombok.RequiredArgsConstructor; 9 | import lombok.experimental.Accessors; 10 | import lombok.experimental.FieldDefaults; 11 | 12 | @Getter 13 | @Accessors(fluent = true) 14 | @RequiredArgsConstructor 15 | @FieldDefaults(level = AccessLevel.PROTECTED, makeFinal = true) 16 | public abstract class TrackableMqttOutMessage extends MqttOutMessage { 17 | 18 | static { 19 | DebugUtils.registerIncludedFields("messageId"); 20 | } 21 | 22 | int messageId; 23 | 24 | @Override 25 | protected void writeVariableHeader(MqttConnection connection, ByteBuffer buffer) { 26 | writeShort(buffer, messageId); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /network/src/main/java/javasabr/mqtt/network/message/out/PublishCompleteMqtt311OutMessage.java: -------------------------------------------------------------------------------- 1 | package javasabr.mqtt.network.message.out; 2 | 3 | import javasabr.mqtt.model.message.MqttMessageType; 4 | import javasabr.mqtt.network.MqttConnection; 5 | 6 | /** 7 | * Publish complete (QoS 2 delivery part 3). 8 | */ 9 | public class PublishCompleteMqtt311OutMessage extends TrackableMqttOutMessage { 10 | 11 | private static final byte MESSAGE_TYPE = (byte) MqttMessageType.PUBLISH_COMPLETE.ordinal(); 12 | 13 | public PublishCompleteMqtt311OutMessage(int messageId) { 14 | super(messageId); 15 | } 16 | 17 | @Override 18 | public int expectedLength(MqttConnection connection) { 19 | return 2; 20 | } 21 | 22 | @Override 23 | protected byte messageTypeId() { 24 | return MESSAGE_TYPE; 25 | } 26 | 27 | @Override 28 | public MqttMessageType messageType() { 29 | return MqttMessageType.PUBLISH_COMPLETE; 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /network/src/main/java/javasabr/mqtt/network/message/out/PublishReceivedMqtt311OutMessage.java: -------------------------------------------------------------------------------- 1 | package javasabr.mqtt.network.message.out; 2 | 3 | import javasabr.mqtt.model.message.MqttMessageType; 4 | import javasabr.mqtt.network.MqttConnection; 5 | 6 | /** 7 | * Publish received (QoS 2 delivery part 1). 8 | */ 9 | public class PublishReceivedMqtt311OutMessage extends TrackableMqttOutMessage { 10 | 11 | private static final byte MESSAGE_TYPE = (byte) MqttMessageType.PUBLISH_RECEIVED.ordinal(); 12 | 13 | public PublishReceivedMqtt311OutMessage(int messageId) { 14 | super(messageId); 15 | } 16 | 17 | @Override 18 | public int expectedLength(MqttConnection connection) { 19 | return 2; 20 | } 21 | 22 | @Override 23 | protected byte messageTypeId() { 24 | return MESSAGE_TYPE; 25 | } 26 | 27 | @Override 28 | public MqttMessageType messageType() { 29 | return MqttMessageType.PUBLISH_RECEIVED; 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /acl-engine/src/main/java/javasabr/mqtt/acl/engine/model/rule/DenyPublishRule.java: -------------------------------------------------------------------------------- 1 | package javasabr.mqtt.acl.engine.model.rule; 2 | 3 | import static javasabr.mqtt.acl.engine.model.Action.DENY; 4 | import static javasabr.mqtt.model.acl.Operation.PUBLISH; 5 | 6 | import javasabr.mqtt.acl.engine.model.Action; 7 | import javasabr.mqtt.acl.engine.model.condition.MqttUserCondition; 8 | import javasabr.mqtt.acl.engine.model.condition.TopicCondition; 9 | import javasabr.mqtt.model.acl.Operation; 10 | import lombok.EqualsAndHashCode; 11 | 12 | @EqualsAndHashCode(callSuper = true) 13 | public final class DenyPublishRule extends AbstractRule { 14 | 15 | public DenyPublishRule(MqttUserCondition userCondition, TopicCondition topicCondition) { 16 | super(userCondition, topicCondition); 17 | } 18 | 19 | @Override 20 | public Operation operation() { 21 | return PUBLISH; 22 | } 23 | 24 | @Override 25 | public Action action() { 26 | return DENY; 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /acl-engine/src/main/java/javasabr/mqtt/acl/engine/model/rule/AllowPublishRule.java: -------------------------------------------------------------------------------- 1 | package javasabr.mqtt.acl.engine.model.rule; 2 | 3 | import static javasabr.mqtt.acl.engine.model.Action.ALLOW; 4 | import static javasabr.mqtt.model.acl.Operation.PUBLISH; 5 | 6 | import javasabr.mqtt.acl.engine.model.Action; 7 | import javasabr.mqtt.acl.engine.model.condition.MqttUserCondition; 8 | import javasabr.mqtt.acl.engine.model.condition.TopicCondition; 9 | import javasabr.mqtt.model.acl.Operation; 10 | import lombok.EqualsAndHashCode; 11 | 12 | @EqualsAndHashCode(callSuper = true) 13 | public final class AllowPublishRule extends AbstractRule { 14 | 15 | public AllowPublishRule(MqttUserCondition userCondition, TopicCondition topicCondition) { 16 | super(userCondition, topicCondition); 17 | } 18 | 19 | @Override 20 | public Operation operation() { 21 | return PUBLISH; 22 | } 23 | 24 | @Override 25 | public Action action() { 26 | return ALLOW; 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /acl-engine/src/main/java/javasabr/mqtt/acl/engine/model/rule/DenySubscribeRule.java: -------------------------------------------------------------------------------- 1 | package javasabr.mqtt.acl.engine.model.rule; 2 | 3 | import static javasabr.mqtt.acl.engine.model.Action.DENY; 4 | import static javasabr.mqtt.model.acl.Operation.SUBSCRIBE; 5 | 6 | import javasabr.mqtt.acl.engine.model.Action; 7 | import javasabr.mqtt.acl.engine.model.condition.MqttUserCondition; 8 | import javasabr.mqtt.acl.engine.model.condition.TopicCondition; 9 | import javasabr.mqtt.model.acl.Operation; 10 | import lombok.EqualsAndHashCode; 11 | 12 | @EqualsAndHashCode(callSuper = true) 13 | public final class DenySubscribeRule extends AbstractRule { 14 | 15 | public DenySubscribeRule(MqttUserCondition userCondition, TopicCondition topicCondition) { 16 | super(userCondition, topicCondition); 17 | } 18 | 19 | @Override 20 | public Operation operation() { 21 | return SUBSCRIBE; 22 | } 23 | 24 | @Override 25 | public Action action() { 26 | return DENY; 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /acl-engine/src/main/java/javasabr/mqtt/acl/engine/model/rule/AllowSubscribeRule.java: -------------------------------------------------------------------------------- 1 | package javasabr.mqtt.acl.engine.model.rule; 2 | 3 | import static javasabr.mqtt.acl.engine.model.Action.ALLOW; 4 | import static javasabr.mqtt.model.acl.Operation.SUBSCRIBE; 5 | 6 | import javasabr.mqtt.acl.engine.model.Action; 7 | import javasabr.mqtt.acl.engine.model.condition.MqttUserCondition; 8 | import javasabr.mqtt.acl.engine.model.condition.TopicCondition; 9 | import javasabr.mqtt.model.acl.Operation; 10 | import lombok.EqualsAndHashCode; 11 | 12 | @EqualsAndHashCode(callSuper = true) 13 | public final class AllowSubscribeRule extends AbstractRule { 14 | 15 | public AllowSubscribeRule(MqttUserCondition userCondition, TopicCondition topicCondition) { 16 | super(userCondition, topicCondition); 17 | } 18 | 19 | @Override 20 | public Operation operation() { 21 | return SUBSCRIBE; 22 | } 23 | 24 | @Override 25 | public Action action() { 26 | return ALLOW; 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /core-service/src/main/java/javasabr/mqtt/service/impl/ExternalNetworkMqttUserFactory.java: -------------------------------------------------------------------------------- 1 | package javasabr.mqtt.service.impl; 2 | 3 | import javasabr.mqtt.network.MqttConnection; 4 | import javasabr.mqtt.network.handler.NetworkMqttUserReleaseHandler; 5 | import javasabr.mqtt.network.impl.ExternalNetworkMqttUser; 6 | import javasabr.mqtt.network.user.ConfigurableNetworkMqttUser; 7 | import javasabr.mqtt.network.user.NetworkMqttUserFactory; 8 | import lombok.AccessLevel; 9 | import lombok.RequiredArgsConstructor; 10 | import lombok.experimental.FieldDefaults; 11 | 12 | @RequiredArgsConstructor 13 | @FieldDefaults(level = AccessLevel.PRIVATE, makeFinal = true) 14 | public class ExternalNetworkMqttUserFactory implements NetworkMqttUserFactory { 15 | 16 | NetworkMqttUserReleaseHandler releaseHandler; 17 | 18 | @Override 19 | public ConfigurableNetworkMqttUser createNetworkUser(MqttConnection connection) { 20 | return new ExternalNetworkMqttUser(connection, releaseHandler); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /network/src/main/java/javasabr/mqtt/network/user/NetworkMqttUser.java: -------------------------------------------------------------------------------- 1 | package javasabr.mqtt.network.user; 2 | 3 | import java.util.concurrent.CompletableFuture; 4 | import javasabr.mqtt.model.MqttUser; 5 | import javasabr.mqtt.network.MqttConnection; 6 | import javasabr.mqtt.network.message.out.MqttOutMessage; 7 | import javasabr.mqtt.network.session.NetworkMqttSession; 8 | import org.jspecify.annotations.Nullable; 9 | 10 | public interface NetworkMqttUser extends MqttUser { 11 | 12 | MqttConnection connection(); 13 | 14 | @Nullable 15 | @Override 16 | NetworkMqttSession session(); 17 | 18 | void sendInBackground(MqttOutMessage message); 19 | 20 | /** 21 | * @return the feature with result of delivering the message 22 | */ 23 | CompletableFuture sendAsync(MqttOutMessage message); 24 | 25 | /** 26 | * @return the feature with result of delivering the reason before closing 27 | */ 28 | CompletableFuture closeWithReason(MqttOutMessage message); 29 | } 30 | -------------------------------------------------------------------------------- /network/src/main/java/javasabr/mqtt/network/message/in/TrackableMqttInMessage.java: -------------------------------------------------------------------------------- 1 | package javasabr.mqtt.network.message.in; 2 | 3 | import java.nio.ByteBuffer; 4 | import javasabr.mqtt.base.util.DebugUtils; 5 | import javasabr.mqtt.model.MqttProperties; 6 | import javasabr.mqtt.network.MqttConnection; 7 | import lombok.AccessLevel; 8 | import lombok.Getter; 9 | import lombok.experimental.Accessors; 10 | import lombok.experimental.FieldDefaults; 11 | 12 | @Getter 13 | @Accessors(fluent = true, chain = false) 14 | @FieldDefaults(level = AccessLevel.PROTECTED) 15 | public abstract class TrackableMqttInMessage extends MqttInMessage { 16 | 17 | static { 18 | DebugUtils.registerIncludedFields("messageId"); 19 | } 20 | 21 | int messageId; 22 | 23 | public TrackableMqttInMessage(byte messageFlags) { 24 | super(messageFlags); 25 | this.messageId = MqttProperties.MESSAGE_ID_IS_NOT_SET; 26 | } 27 | 28 | @Override 29 | protected void readVariableHeader(MqttConnection connection, ByteBuffer buffer) { 30 | messageId = readShortUnsigned(buffer); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /core-service/src/main/java/javasabr/mqtt/service/session/impl/InMemoryTrackedMessageMeta.java: -------------------------------------------------------------------------------- 1 | package javasabr.mqtt.service.session.impl; 2 | 3 | import javasabr.mqtt.base.util.DebugUtils; 4 | import javasabr.mqtt.model.message.MqttMessageType; 5 | import javasabr.mqtt.model.reason.code.ReasonCode; 6 | import javasabr.mqtt.model.session.TrackedMessageMeta; 7 | import lombok.AccessLevel; 8 | import lombok.AllArgsConstructor; 9 | import lombok.Getter; 10 | import lombok.Setter; 11 | import lombok.experimental.Accessors; 12 | import lombok.experimental.FieldDefaults; 13 | import org.jspecify.annotations.Nullable; 14 | 15 | @Getter 16 | @Setter 17 | @Accessors 18 | @AllArgsConstructor 19 | @FieldDefaults(level = AccessLevel.PRIVATE) 20 | public class InMemoryTrackedMessageMeta implements TrackedMessageMeta { 21 | 22 | static { 23 | DebugUtils.registerIncludedFields("messageType", "reasonCode"); 24 | } 25 | 26 | MqttMessageType messageType; 27 | @Nullable 28 | ReasonCode reasonCode; 29 | 30 | @Override 31 | public String toString() { 32 | return DebugUtils.toJsonString(this); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /application/build.gradle: -------------------------------------------------------------------------------- 1 | plugins { 2 | id("java-library") 3 | id("configure-java") 4 | id("groovy") 5 | id("org.springframework.boot") 6 | } 7 | 8 | description = "Standard configuration of standalone version of MQTT Broker" 9 | 10 | dependencies { 11 | implementation projects.coreService 12 | implementation projects.aclService 13 | implementation projects.aclGroovyDsl 14 | implementation libs.rlib.logger.slf4j 15 | implementation libs.springboot.starter.core 16 | implementation libs.springboot.starter.log4j2 17 | 18 | testImplementation projects.testSupport 19 | testImplementation testFixtures(projects.network) 20 | } 21 | 22 | tasks.withType(GroovyCompile).configureEach { 23 | options.forkOptions.jvmArgs += "--enable-preview" 24 | } 25 | 26 | configurations.each { 27 | it.exclude group: "org.slf4j", module: "slf4j-log4j12" 28 | it.exclude group: "org.springframework.boot", module: "spring-boot-starter-logging" 29 | } 30 | 31 | bootRun { 32 | mainClass = "javasabr.mqtt.broker.application.MqttBrokerApplication" 33 | } 34 | 35 | bootJar { 36 | mainClass = "javasabr.mqtt.broker.application.MqttBrokerApplication" 37 | } 38 | -------------------------------------------------------------------------------- /acl-engine/src/main/java/javasabr/mqtt/acl/engine/model/rule/AbstractRule.java: -------------------------------------------------------------------------------- 1 | package javasabr.mqtt.acl.engine.model.rule; 2 | 3 | import javasabr.mqtt.acl.engine.model.condition.MqttUserCondition; 4 | import javasabr.mqtt.acl.engine.model.condition.TopicCondition; 5 | import javasabr.mqtt.model.MqttUser; 6 | import javasabr.mqtt.model.acl.Operation; 7 | import javasabr.mqtt.model.topic.AbstractTopic; 8 | import lombok.AccessLevel; 9 | import lombok.EqualsAndHashCode; 10 | import lombok.Getter; 11 | import lombok.RequiredArgsConstructor; 12 | import lombok.experimental.Accessors; 13 | import lombok.experimental.FieldDefaults; 14 | 15 | @Getter 16 | @Accessors(fluent = true) 17 | @RequiredArgsConstructor 18 | @FieldDefaults(makeFinal = true, level = AccessLevel.PRIVATE) 19 | @EqualsAndHashCode 20 | public abstract class AbstractRule implements Rule { 21 | 22 | MqttUserCondition userCondition; 23 | TopicCondition topicCondition; 24 | 25 | @Override 26 | public boolean test(MqttUser mqttUser, Operation operation, AbstractTopic topic) { 27 | return operation() == operation && topicCondition.test(topic) && userCondition.test(mqttUser); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /network/src/main/java/javasabr/mqtt/network/message/out/UnsubscribeAckMqtt311OutMessage.java: -------------------------------------------------------------------------------- 1 | package javasabr.mqtt.network.message.out; 2 | 3 | import javasabr.mqtt.model.message.MqttMessageType; 4 | import javasabr.mqtt.network.MqttConnection; 5 | import lombok.AccessLevel; 6 | import lombok.Getter; 7 | import lombok.experimental.Accessors; 8 | import lombok.experimental.FieldDefaults; 9 | 10 | /** 11 | * Unsubscribe acknowledgement. 12 | */ 13 | @Getter 14 | @Accessors 15 | @FieldDefaults(level = AccessLevel.PROTECTED, makeFinal = true) 16 | public class UnsubscribeAckMqtt311OutMessage extends TrackableMqttOutMessage { 17 | 18 | private static final byte MESSAGE_TYPE = (byte) MqttMessageType.UNSUBSCRIBE_ACK.ordinal(); 19 | 20 | public UnsubscribeAckMqtt311OutMessage(int messageId) { 21 | super(messageId); 22 | } 23 | 24 | @Override 25 | public int expectedLength(MqttConnection connection) { 26 | return 2; 27 | } 28 | 29 | @Override 30 | protected byte messageTypeId() { 31 | return MESSAGE_TYPE; 32 | } 33 | 34 | @Override 35 | public MqttMessageType messageType() { 36 | return MqttMessageType.UNSUBSCRIBE_ACK; 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /acl-groovy-dsl/src/test/resources/acl/config/acl.groovy: -------------------------------------------------------------------------------- 1 | package acl.config 2 | 3 | allowPublish { 4 | anyOf { 5 | userName eq("sensor1"), regex("sensor10\$") 6 | clientId eq("clientId1"), regex("^cliend") 7 | ipAddress eq("10.56.0.3"), eq("127.0.0.1") 8 | anyOf { 9 | userName anyone() 10 | clientId eq("clientId2") 11 | ipAddress eq("10.56.0.3") 12 | } 13 | allOf { 14 | userName eq("sensor2") 15 | clientId eq("clientId2") 16 | ipAddress eq("10.56.0.3") 17 | } 18 | } 19 | topicName eq("/topic1"), eq("/topic2/temp") 20 | } 21 | 22 | denySubscribe { 23 | allOf { 24 | userName eq("sensor2") 25 | clientId eq("clientId2") 26 | ipAddress eq("10.56.0.3") 27 | } 28 | topicFilter match("/topic1/#") 29 | topicFilter match("/topic2/+/temp") 30 | } 31 | 32 | allowSubscribe { 33 | allOf { 34 | userName eq("sensor2") 35 | clientId eq("clientId2") 36 | ipAddress eq("10.56.0.3") 37 | } 38 | topicFilter match("/topic1/#") 39 | topicFilter match("/topic2/+/temp") 40 | } 41 | 42 | denyPublish { 43 | anyOf() 44 | topicName anyone() 45 | } 46 | 47 | denySubscribe { 48 | anyOf() 49 | topicFilter anyone() 50 | } 51 | -------------------------------------------------------------------------------- /model/src/testFixtures/groovy/javasabr/mqtt/model/subscription/TestMqttUser.groovy: -------------------------------------------------------------------------------- 1 | package javasabr.mqtt.model.subscription 2 | 3 | import com.fasterxml.jackson.annotation.JsonValue 4 | import javasabr.mqtt.model.MqttClientConnectionConfig 5 | import javasabr.mqtt.model.MqttUser 6 | import javasabr.mqtt.model.message.SendableMqttMessage 7 | import javasabr.mqtt.model.session.MqttSession 8 | 9 | import java.util.concurrent.CompletableFuture 10 | import java.util.concurrent.CompletionStage 11 | 12 | record TestMqttUser(String clientId, String userName, String ipAddress) implements MqttUser { 13 | 14 | TestMqttUser(String id) { 15 | this(id, null, "localhost") 16 | } 17 | 18 | @JsonValue 19 | @Override 20 | String toString() { 21 | return clientId 22 | } 23 | 24 | @Override 25 | MqttSession session() { 26 | return null 27 | } 28 | 29 | @Override 30 | MqttClientConnectionConfig connectionConfig() { 31 | return null 32 | } 33 | 34 | @Override 35 | void sendInBackground(SendableMqttMessage message) { 36 | } 37 | 38 | @Override 39 | CompletionStage sendAsync(SendableMqttMessage message) { 40 | return CompletableFuture.completedFuture(true) 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /model/src/main/java/javasabr/mqtt/model/reason/code/AuthenticateReasonCode.java: -------------------------------------------------------------------------------- 1 | package javasabr.mqtt.model.reason.code; 2 | 3 | import javasabr.rlib.common.util.NumberedEnum; 4 | import javasabr.rlib.common.util.NumberedEnumMap; 5 | import lombok.Getter; 6 | import lombok.RequiredArgsConstructor; 7 | import lombok.experimental.Accessors; 8 | 9 | @Getter 10 | @Accessors 11 | @RequiredArgsConstructor 12 | public enum AuthenticateReasonCode implements NumberedEnum, ReasonCode { 13 | 14 | /** 15 | * Authentication is successful. Server. 16 | */ 17 | SUCCESS(0x00), 18 | /** 19 | * Continue the authentication with another step. Client or Server. 20 | */ 21 | CONTINUE_AUTHENTICATION(0x18), 22 | /** 23 | * Initiate a re-authentication. Client. 24 | */ 25 | RE_AUTHENTICATE(0x19); 26 | 27 | private static final NumberedEnumMap NUMBERED_MAP = 28 | new NumberedEnumMap<>(AuthenticateReasonCode.class); 29 | 30 | public static AuthenticateReasonCode ofCode(int code) { 31 | return NUMBERED_MAP.require(code); 32 | } 33 | 34 | private final int code; 35 | 36 | @Override 37 | public int number() { 38 | return code; 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /model/src/main/java/javasabr/mqtt/model/reason/code/PublishReleaseReasonCode.java: -------------------------------------------------------------------------------- 1 | package javasabr.mqtt.model.reason.code; 2 | 3 | import javasabr.rlib.common.util.NumberedEnum; 4 | import javasabr.rlib.common.util.NumberedEnumMap; 5 | import lombok.Getter; 6 | import lombok.RequiredArgsConstructor; 7 | import lombok.experimental.Accessors; 8 | 9 | @Getter 10 | @Accessors 11 | @RequiredArgsConstructor 12 | public enum PublishReleaseReasonCode implements NumberedEnum, ReasonCode { 13 | 14 | /** 15 | * Message released. 16 | */ 17 | SUCCESS(0x00), 18 | /** 19 | * The Packet Identifier is not known. This is not an error during recovery, but at other times indicates a mismatch 20 | * between the Session State on the Client and Server. 21 | */ 22 | PACKET_IDENTIFIER_NOT_FOUND(0x92); 23 | 24 | private static final NumberedEnumMap NUMBERED_MAP = 25 | new NumberedEnumMap<>(PublishReleaseReasonCode.class); 26 | 27 | public static PublishReleaseReasonCode ofCode(int code) { 28 | return NUMBERED_MAP.require(code); 29 | } 30 | 31 | private final int code; 32 | 33 | @Override 34 | public int number() { 35 | return code; 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /core-service/src/main/java/javasabr/mqtt/service/publish/handler/impl/Qos0MqttPublishOutMessageHandler.java: -------------------------------------------------------------------------------- 1 | package javasabr.mqtt.service.publish.handler.impl; 2 | 3 | import javasabr.mqtt.model.MqttProperties; 4 | import javasabr.mqtt.model.QoS; 5 | import javasabr.mqtt.model.publishing.Publish; 6 | import javasabr.mqtt.model.session.MqttSession; 7 | import javasabr.mqtt.network.impl.ExternalNetworkMqttUser; 8 | import javasabr.mqtt.service.MessageOutFactoryService; 9 | import org.jspecify.annotations.Nullable; 10 | 11 | public class Qos0MqttPublishOutMessageHandler 12 | extends AbstractMqttPublishOutMessageHandler { 13 | 14 | public Qos0MqttPublishOutMessageHandler(MessageOutFactoryService messageOutFactoryService) { 15 | super(ExternalNetworkMqttUser.class, messageOutFactoryService); 16 | } 17 | 18 | @Override 19 | public QoS qos() { 20 | return QoS.AT_MOST_ONCE; 21 | } 22 | 23 | @Nullable 24 | @Override 25 | protected Publish reconstruct( 26 | ExternalNetworkMqttUser user, 27 | MqttSession session, 28 | Publish original) { 29 | return original.with( 30 | MqttProperties.MESSAGE_ID_IS_NOT_SET, 31 | qos(), 32 | false, 33 | MqttProperties.TOPIC_ALIAS_NOT_SET); 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /model/src/main/java/javasabr/mqtt/model/reason/code/PublishCompletedReasonCode.java: -------------------------------------------------------------------------------- 1 | package javasabr.mqtt.model.reason.code; 2 | 3 | import javasabr.rlib.common.util.NumberedEnum; 4 | import javasabr.rlib.common.util.NumberedEnumMap; 5 | import lombok.Getter; 6 | import lombok.RequiredArgsConstructor; 7 | import lombok.experimental.Accessors; 8 | 9 | @Getter 10 | @Accessors 11 | @RequiredArgsConstructor 12 | public enum PublishCompletedReasonCode implements NumberedEnum, ReasonCode { 13 | 14 | /** 15 | * Packet Identifier released. Publication of QoS 2 message is complete. 16 | */ 17 | SUCCESS(0x00), 18 | /** 19 | * The Packet Identifier is not known. This is not an error during recovery, but at other times indicates a mismatch 20 | * between the Session State on the Client and Server. 21 | */ 22 | PACKET_IDENTIFIER_NOT_FOUND(0x92); 23 | 24 | private static final NumberedEnumMap NUMBERED_MAP = 25 | new NumberedEnumMap<>(PublishCompletedReasonCode.class); 26 | 27 | public static PublishCompletedReasonCode ofCode(int code) { 28 | return NUMBERED_MAP.require(code); 29 | } 30 | 31 | private final int code; 32 | 33 | @Override 34 | public int number() { 35 | return code; 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /model/src/main/java/javasabr/mqtt/model/topic/TopicFilter.java: -------------------------------------------------------------------------------- 1 | package javasabr.mqtt.model.topic; 2 | 3 | import lombok.AccessLevel; 4 | import lombok.Getter; 5 | import lombok.experimental.Accessors; 6 | import lombok.experimental.FieldDefaults; 7 | 8 | @Getter 9 | @Accessors(fluent = true) 10 | @FieldDefaults(level = AccessLevel.PROTECTED, makeFinal = true) 11 | public class TopicFilter extends AbstractTopic { 12 | 13 | public static final String MULTI_LEVEL_WILDCARD = "#"; 14 | public static final char MULTI_LEVEL_WILDCARD_CHAR = '#'; 15 | public static final String SINGLE_LEVEL_WILDCARD = "+"; 16 | public static final char SINGLE_LEVEL_WILDCARD_CHAR = '+'; 17 | public static final String SPECIAL = "$"; 18 | 19 | public static final TopicFilter INVALID_TOPIC_FILTER = new TopicFilter("$invalid$") { 20 | @Override 21 | public boolean isInvalid() { 22 | return true; 23 | } 24 | }; 25 | 26 | boolean wildcard; 27 | 28 | public TopicFilter(String rawTopicFilter) { 29 | super(rawTopicFilter); 30 | this.wildcard = rawTopicFilter.contains(SINGLE_LEVEL_WILDCARD) || rawTopicFilter.contains(MULTI_LEVEL_WILDCARD); 31 | } 32 | 33 | public static TopicFilter valueOf(String rawTopicFilter) { 34 | return new TopicFilter(rawTopicFilter); 35 | } 36 | } 37 | 38 | -------------------------------------------------------------------------------- /model/src/main/java/javasabr/mqtt/model/MqttClientConnectionConfig.java: -------------------------------------------------------------------------------- 1 | package javasabr.mqtt.model; 2 | 3 | public record MqttClientConnectionConfig( 4 | MqttServerConnectionConfig server, 5 | QoS maxQos, 6 | MqttVersion mqttVersion, 7 | long sessionExpiryInterval, 8 | int receiveMaxPublishes, 9 | int maxMessageSize, 10 | int topicAliasMaxValue, 11 | int keepAlive, 12 | boolean requestResponseInformation, 13 | boolean requestProblemInformation) { 14 | 15 | public boolean subscriptionIdAvailable() { 16 | return server.subscriptionIdAvailable(); 17 | } 18 | 19 | public boolean retainAvailable() { 20 | return server.retainAvailable(); 21 | } 22 | 23 | public boolean wildcardSubscriptionAvailable() { 24 | return server.wildcardSubscriptionAvailable(); 25 | } 26 | 27 | public boolean sharedSubscriptionAvailable() { 28 | return server.sharedSubscriptionAvailable(); 29 | } 30 | 31 | public boolean sessionsEnabled() { 32 | return server.sessionsEnabled(); 33 | } 34 | 35 | public int maxTopicLevels() { 36 | return server.maxTopicLevels(); 37 | } 38 | 39 | public int maxStringLength() { 40 | return server.maxStringLength(); 41 | } 42 | 43 | public int maxBinarySize() { 44 | return server.maxBinarySize(); 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /acl-service/src/main/java/javasabr/mqtt/acl/service/AclEngineBasedAuthorizationService.java: -------------------------------------------------------------------------------- 1 | package javasabr.mqtt.acl.service; 2 | 3 | import javasabr.mqtt.acl.engine.AclEngine; 4 | import javasabr.mqtt.model.MqttUser; 5 | import javasabr.mqtt.model.acl.Operation; 6 | import javasabr.mqtt.model.topic.TopicFilter; 7 | import javasabr.mqtt.model.topic.TopicName; 8 | import javasabr.mqtt.service.AuthorizationService; 9 | 10 | public abstract class AclEngineBasedAuthorizationService implements AuthorizationService { 11 | 12 | protected AclEngine engine; 13 | 14 | protected AclEngineBasedAuthorizationService() { 15 | this.engine = AclEngine.NO_OPS_ENGINE; 16 | } 17 | 18 | @Override 19 | public boolean authorizePublish(MqttUser user, TopicName topicName) { 20 | return engine.authorize(user, Operation.PUBLISH, topicName); 21 | } 22 | 23 | @Override 24 | public boolean authorizeSubscribe(MqttUser user, TopicFilter topicFilter) { 25 | return engine.authorize(user, Operation.SUBSCRIBE, topicFilter); 26 | } 27 | 28 | // we know that writing this to not volatile field will not apply it for all threads immediately, 29 | // but for us it's not critical comparing to cost of reading volatile field 30 | protected synchronized void switchTo(AclEngine newEngine) { 31 | this.engine = newEngine; 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /acl-service/src/main/java/javasabr/mqtt/acl/service/conifg/GroovyDslBasedAclServiceSpringConfig.java: -------------------------------------------------------------------------------- 1 | package javasabr.mqtt.acl.service.conifg; 2 | 3 | import java.net.URI; 4 | import javasabr.mqtt.acl.service.impl.GroovyDslBasedAuthorizationService; 5 | import javasabr.mqtt.service.AuthorizationService; 6 | import lombok.CustomLog; 7 | import org.springframework.beans.factory.annotation.Value; 8 | import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; 9 | import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; 10 | import org.springframework.context.annotation.Bean; 11 | import org.springframework.context.annotation.Configuration; 12 | 13 | @CustomLog 14 | @Configuration(proxyBeanMethods = false) 15 | @ConditionalOnProperty(name = "acl.engine.type", havingValue = "groovy-dsl") 16 | @ConditionalOnClass(name = "javasabr.mqtt.acl.groovy.dsl.loader.AclRulesLoader") 17 | public class GroovyDslBasedAclServiceSpringConfig { 18 | 19 | @Bean 20 | AuthorizationService authorizationService(@Value("${acl.engine.groovy.dsl.config}") URI aclConfigUri) { 21 | log.info("Initializing Groovy-DSL based AuthorizationService..."); 22 | var authorizationService = new GroovyDslBasedAuthorizationService(); 23 | authorizationService.loadFrom(aclConfigUri); 24 | return authorizationService; 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /model/src/main/java/javasabr/mqtt/model/topic/SharedTopicFilter.java: -------------------------------------------------------------------------------- 1 | package javasabr.mqtt.model.topic; 2 | 3 | import lombok.AccessLevel; 4 | import lombok.Getter; 5 | import lombok.experimental.Accessors; 6 | import lombok.experimental.FieldDefaults; 7 | 8 | @Getter 9 | @Accessors(fluent = true, chain = false) 10 | @FieldDefaults(level = AccessLevel.PRIVATE, makeFinal = true) 11 | public class SharedTopicFilter extends TopicFilter { 12 | 13 | public static final String SHARE_KEYWORD = "$share"; 14 | 15 | String shareName; 16 | 17 | public SharedTopicFilter(String rawTopicFilter, String shareName) { 18 | super(rawTopicFilter); 19 | this.shareName = shareName; 20 | } 21 | 22 | public static SharedTopicFilter valueOf(String rawSharedTopicFilter) { 23 | // $share/{ShareName}/{filter} 24 | int firstSlash = rawSharedTopicFilter.indexOf(DELIMITER) + 1; 25 | int secondSlash = rawSharedTopicFilter.indexOf(DELIMITER, firstSlash); 26 | String shareName = rawSharedTopicFilter.substring(firstSlash, secondSlash); 27 | String rawTopicFilter = rawSharedTopicFilter.substring(secondSlash + 1); 28 | return new SharedTopicFilter(rawTopicFilter, shareName); 29 | } 30 | 31 | public static boolean isShared(String rawTopicFilter) { 32 | return rawTopicFilter.startsWith(SharedTopicFilter.SHARE_KEYWORD); 33 | } 34 | } 35 | 36 | -------------------------------------------------------------------------------- /network/src/test/groovy/javasabr/mqtt/network/message/out/DisconnectAckMqtt5OutMessageTest.groovy: -------------------------------------------------------------------------------- 1 | package javasabr.mqtt.network.message.out 2 | 3 | import javasabr.mqtt.model.reason.code.DisconnectReasonCode 4 | import javasabr.mqtt.network.message.in.DisconnectMqttInMessage 5 | import javasabr.rlib.common.util.BufferUtils 6 | 7 | class DisconnectAckMqtt5OutMessageTest extends BaseMqttOutMessageTest { 8 | 9 | def "should write packet correctly"() { 10 | given: 11 | def packet = new DisconnectMqtt5OutMessage( 12 | DisconnectReasonCode.PACKET_TOO_LARGE, 13 | testUserProperties, 14 | reasonString, 15 | serverReference, 16 | sessionExpiryInterval) 17 | when: 18 | def dataBuffer = BufferUtils.prepareBuffer(512) { 19 | packet.write(defaultMqtt5Connection, it) 20 | } 21 | def reader = new DisconnectMqttInMessage(0b1110_0000 as byte) 22 | def result = reader.read(defaultMqtt5Connection, dataBuffer, dataBuffer.limit()) 23 | then: 24 | result 25 | reader.reasonCode == DisconnectReasonCode.PACKET_TOO_LARGE 26 | reader.userProperties() == testUserProperties 27 | reader.reason == reasonString 28 | reader.serverReference == serverReference 29 | reader.sessionExpiryInterval == sessionExpiryInterval 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /core-service/src/main/java/javasabr/mqtt/service/impl/DefaultMqttConnectionFactory.java: -------------------------------------------------------------------------------- 1 | package javasabr.mqtt.service.impl; 2 | 3 | import java.nio.channels.AsynchronousSocketChannel; 4 | import javasabr.mqtt.model.MqttServerConnectionConfig; 5 | import javasabr.mqtt.network.MqttConnection; 6 | import javasabr.mqtt.network.MqttConnectionFactory; 7 | import javasabr.mqtt.network.user.NetworkMqttUserFactory; 8 | import javasabr.rlib.network.Network; 9 | import javasabr.rlib.network.impl.DefaultBufferAllocator; 10 | import lombok.AccessLevel; 11 | import lombok.RequiredArgsConstructor; 12 | import lombok.experimental.FieldDefaults; 13 | 14 | @RequiredArgsConstructor 15 | @FieldDefaults(level = AccessLevel.PRIVATE, makeFinal = true) 16 | public class DefaultMqttConnectionFactory implements MqttConnectionFactory { 17 | 18 | MqttServerConnectionConfig serverConnectionConfig; 19 | NetworkMqttUserFactory clientFactory; 20 | int maxPacketsByRead; 21 | 22 | @Override 23 | public MqttConnection newConnection(Network network, AsynchronousSocketChannel channel) { 24 | DefaultBufferAllocator bufferAllocator = new DefaultBufferAllocator(network.config()); 25 | return new MqttConnection( 26 | network, 27 | channel, 28 | bufferAllocator, 29 | maxPacketsByRead, 30 | serverConnectionConfig, 31 | clientFactory); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /network/src/main/java/javasabr/mqtt/network/message/out/PublishReleaseMqtt311OutMessage.java: -------------------------------------------------------------------------------- 1 | package javasabr.mqtt.network.message.out; 2 | 3 | import java.nio.ByteBuffer; 4 | import javasabr.mqtt.model.message.MqttMessageType; 5 | import javasabr.mqtt.network.MqttConnection; 6 | 7 | /** 8 | * Publish release (QoS 2 delivery part 2). 9 | */ 10 | public class PublishReleaseMqtt311OutMessage extends TrackableMqttOutMessage { 11 | 12 | public static final int MESSAGE_FLAGS = 0b0000_0010; 13 | private static final byte MESSAGE_TYPE = (byte) MqttMessageType.PUBLISH_RELEASE.ordinal(); 14 | 15 | public PublishReleaseMqtt311OutMessage(int messageId) { 16 | super(messageId); 17 | } 18 | 19 | @Override 20 | protected byte messageTypeId() { 21 | return MESSAGE_TYPE; 22 | } 23 | 24 | @Override 25 | public MqttMessageType messageType() { 26 | return MqttMessageType.PUBLISH_RELEASE; 27 | } 28 | 29 | @Override 30 | protected byte messageFlags() { 31 | return MESSAGE_FLAGS; 32 | } 33 | 34 | @Override 35 | public int expectedLength(MqttConnection connection) { 36 | return PACKET_ID_SIZE; 37 | } 38 | 39 | @Override 40 | protected void writeVariableHeader(MqttConnection connection, ByteBuffer buffer) { 41 | // http://docs.oasis-open.org/mqtt/mqtt/v3.1.1/os/mqtt-v3.1.1-os.html#_Toc398718055 42 | writeShort(buffer, messageId); 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /core-service/src/main/java/javasabr/mqtt/service/message/handler/impl/ProcessingOutPublishesMqttInMessageHandler.java: -------------------------------------------------------------------------------- 1 | package javasabr.mqtt.service.message.handler.impl; 2 | 3 | import javasabr.mqtt.model.message.TrackableMqttMessage; 4 | import javasabr.mqtt.model.session.ProcessingPublishes; 5 | import javasabr.mqtt.network.MqttConnection; 6 | import javasabr.mqtt.network.impl.ExternalNetworkMqttUser; 7 | import javasabr.mqtt.network.message.in.MqttInMessage; 8 | import javasabr.mqtt.network.session.NetworkMqttSession; 9 | import javasabr.mqtt.service.MessageOutFactoryService; 10 | 11 | public abstract class ProcessingOutPublishesMqttInMessageHandler 12 | extends AbstractMqttInMessageHandler { 13 | 14 | protected ProcessingOutPublishesMqttInMessageHandler( 15 | Class expectedNetworkPacket, 16 | MessageOutFactoryService messageOutFactoryService) { 17 | super(ExternalNetworkMqttUser.class, expectedNetworkPacket, messageOutFactoryService); 18 | } 19 | 20 | @Override 21 | protected void processValidMessage( 22 | MqttConnection connection, 23 | ExternalNetworkMqttUser user, 24 | NetworkMqttSession session, 25 | M message) { 26 | ProcessingPublishes processingPublishes = session.outProcessingPublishes(); 27 | processingPublishes.apply(user, message); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /acl-engine/src/main/java/javasabr/mqtt/acl/engine/model/matcher/TopicFilterMatcher.java: -------------------------------------------------------------------------------- 1 | package javasabr.mqtt.acl.engine.model.matcher; 2 | 3 | import java.util.Objects; 4 | import javasabr.mqtt.model.topic.AbstractTopic; 5 | import javasabr.mqtt.model.topic.TopicFilter; 6 | 7 | public record TopicFilterMatcher(TopicFilter expectedTopic) implements ValueMatcher { 8 | 9 | @Override 10 | public boolean test(AbstractTopic requestedTopic) { 11 | return matches(requestedTopic); 12 | } 13 | 14 | private boolean matches(AbstractTopic requestedTopicFilter) { 15 | final int expectedFilterLevels = expectedTopic.levelsCount(); 16 | final int incomingFilterLevels = requestedTopicFilter.levelsCount(); 17 | for (int i = 0; i < expectedFilterLevels; i++) { 18 | String expectedSegment = expectedTopic.segment(i); 19 | if (Objects.equals(expectedSegment, TopicFilter.MULTI_LEVEL_WILDCARD)) { 20 | return i == expectedFilterLevels - 1; 21 | } else if (i >= incomingFilterLevels) { 22 | return false; 23 | } 24 | String requestedSegment = requestedTopicFilter.segment(i); 25 | if (Objects.equals(expectedSegment, TopicFilter.SINGLE_LEVEL_WILDCARD)) { 26 | continue; 27 | } 28 | if (!Objects.equals(expectedSegment, requestedSegment)) { 29 | return false; 30 | } 31 | } 32 | return expectedFilterLevels == incomingFilterLevels; 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /network/src/test/groovy/javasabr/mqtt/network/message/out/ConnectMqtt311OutMessageTest.groovy: -------------------------------------------------------------------------------- 1 | package javasabr.mqtt.network.message.out 2 | 3 | import javasabr.mqtt.model.QoS 4 | import javasabr.mqtt.network.message.in.ConnectMqttInMessage 5 | import javasabr.rlib.common.util.ArrayUtils 6 | import javasabr.rlib.common.util.BufferUtils 7 | 8 | class ConnectMqtt311OutMessageTest extends BaseMqttOutMessageTest { 9 | 10 | def "should write packet correctly"() { 11 | given: 12 | def packet = new ConnectMqtt311OutMessage( 13 | userName, 14 | "", 15 | mqtt311ClientId, 16 | userPassword, 17 | ArrayUtils.EMPTY_BYTE_ARRAY, 18 | QoS.AT_MOST_ONCE, 19 | keepAlive, 20 | willRetain, 21 | cleanStart) 22 | when: 23 | def dataBuffer = BufferUtils.prepareBuffer(512) { 24 | packet.write(defaultMqtt311Connection, it) 25 | } 26 | def reader = new ConnectMqttInMessage(0b0001_0000 as byte) 27 | def result = reader.read(defaultMqtt311Connection, dataBuffer, dataBuffer.limit()) 28 | then: 29 | result 30 | reader.username() == userName 31 | reader.clientId() == mqtt311ClientId 32 | reader.password() == userPassword 33 | reader.keepAlive() == keepAlive 34 | reader.userProperties() == MqttOutMessage.EMPTY_USER_PROPERTIES 35 | reader.cleanStart() == cleanStart 36 | reader.willRetain == willRetain 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /acl-engine/src/main/java/javasabr/mqtt/acl/engine/AclEngine.java: -------------------------------------------------------------------------------- 1 | package javasabr.mqtt.acl.engine; 2 | 3 | import java.util.Arrays; 4 | import java.util.EnumMap; 5 | import java.util.Map; 6 | import java.util.stream.Collectors; 7 | import javasabr.mqtt.acl.engine.model.Action; 8 | import javasabr.mqtt.acl.engine.model.rule.Rule; 9 | import javasabr.mqtt.model.MqttUser; 10 | import javasabr.mqtt.model.acl.Operation; 11 | import javasabr.mqtt.model.topic.AbstractTopic; 12 | import javasabr.rlib.collections.array.Array; 13 | import lombok.AccessLevel; 14 | import lombok.RequiredArgsConstructor; 15 | import lombok.experimental.FieldDefaults; 16 | 17 | @RequiredArgsConstructor 18 | @FieldDefaults(level = AccessLevel.PRIVATE, makeFinal = true) 19 | public final class AclEngine { 20 | 21 | public static final AclEngine NO_OPS_ENGINE; 22 | 23 | static { 24 | Map> emptyRules = Arrays 25 | .stream(Operation.values()) 26 | .collect(Collectors.toMap(operation -> operation, _ -> Array.empty(Rule.class))); 27 | NO_OPS_ENGINE = new AclEngine(new EnumMap<>(emptyRules)); 28 | } 29 | 30 | Map> ruleMap; 31 | 32 | public boolean authorize(MqttUser mqttUser, Operation operation, AbstractTopic topic) { 33 | Array rules = ruleMap.get(operation); 34 | for (Rule rule : rules) { 35 | if (rule.test(mqttUser, operation, topic)) { 36 | return rule.action() == Action.ALLOW; 37 | } 38 | } 39 | return false; 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /base/src/test/groovy/javasabr/mqtt/base/util/DebugUtilsTest.groovy: -------------------------------------------------------------------------------- 1 | package javasabr.mqtt.base.util 2 | 3 | import javasabr.mqtt.test.support.UnitSpecification 4 | import javasabr.rlib.collections.array.Array 5 | import javasabr.rlib.collections.array.ArrayFactory 6 | import javasabr.rlib.collections.array.MutableArray 7 | 8 | class DebugUtilsTest extends UnitSpecification { 9 | 10 | class TestData { 11 | String name = "testData" 12 | String ignored = "ignored" 13 | MutableArray mutableValues = ArrayFactory.mutableArray(String) 14 | Array values = ArrayFactory.mutableArray(String) 15 | Iterable emptyArray = Array.empty(String) 16 | 17 | TestData() { 18 | def array = ArrayFactory.mutableArray(String) 19 | array.add("First") 20 | array.add("Second") 21 | this.mutableValues = array; 22 | this.values = Array.copyOf(array) 23 | } 24 | 25 | static { 26 | DebugUtils.registerIncludedFields(TestData, 27 | "name", "mutableValues", "values", "emptyArray") 28 | } 29 | 30 | String toString() { 31 | return DebugUtils.toJsonString(this) 32 | } 33 | } 34 | 35 | def "should correctly write class to json"() { 36 | given: 37 | def data = new TestData() 38 | when: 39 | def json = data.toString() 40 | then: 41 | json == """{ 42 | "emptyArray" : [ ], 43 | "mutableValues" : [ "First", "Second" ], 44 | "name" : "testData", 45 | "values" : [ "First", "Second" ] 46 | }""" 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /core-service/src/main/java/javasabr/mqtt/service/impl/AbstractCredentialSource.java: -------------------------------------------------------------------------------- 1 | package javasabr.mqtt.service.impl; 2 | 3 | import java.util.Arrays; 4 | import javasabr.mqtt.service.CredentialSource; 5 | import javasabr.rlib.collections.dictionary.DictionaryFactory; 6 | import javasabr.rlib.collections.dictionary.LockableRefToRefDictionary; 7 | import javasabr.rlib.collections.dictionary.RefToRefDictionary; 8 | import reactor.core.publisher.Mono; 9 | 10 | public abstract class AbstractCredentialSource implements CredentialSource { 11 | 12 | private final LockableRefToRefDictionary credentials = 13 | DictionaryFactory.stampedLockBasedRefToRefDictionary(String.class, byte[].class); 14 | 15 | abstract void init(); 16 | 17 | void putAll(RefToRefDictionary otherCredentials) { 18 | long stamp = credentials.writeLock(); 19 | try { 20 | credentials.append(otherCredentials); 21 | } finally { 22 | credentials.writeUnlock(stamp); 23 | } 24 | } 25 | 26 | void put(String user, byte[] pass) { 27 | long stamp = credentials.writeLock(); 28 | try { 29 | credentials.put(user, pass); 30 | } finally { 31 | credentials.writeUnlock(stamp); 32 | } 33 | } 34 | 35 | @Override 36 | public Mono check(String user, byte[] pass) { 37 | return Mono.just(Arrays.equals(pass, credentials.get(user))); 38 | } 39 | 40 | @Override 41 | public Mono check(byte[] pass) { 42 | return Mono.just(Boolean.FALSE); 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /application/src/test/resources/log4j2.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | -------------------------------------------------------------------------------- /model/src/main/java/javasabr/mqtt/model/QoS.java: -------------------------------------------------------------------------------- 1 | package javasabr.mqtt.model; 2 | 3 | import javasabr.mqtt.model.reason.code.SubscribeAckReasonCode; 4 | import javasabr.rlib.common.util.NumberedEnum; 5 | import javasabr.rlib.common.util.NumberedEnumMap; 6 | import lombok.AccessLevel; 7 | import lombok.Getter; 8 | import lombok.RequiredArgsConstructor; 9 | import lombok.experimental.Accessors; 10 | import lombok.experimental.FieldDefaults; 11 | 12 | @Getter 13 | @Accessors 14 | @RequiredArgsConstructor 15 | @FieldDefaults(level = AccessLevel.PRIVATE, makeFinal = true) 16 | public enum QoS implements NumberedEnum { 17 | AT_MOST_ONCE(0, SubscribeAckReasonCode.GRANTED_QOS_0), 18 | AT_LEAST_ONCE(1, SubscribeAckReasonCode.GRANTED_QOS_1), 19 | EXACTLY_ONCE(2, SubscribeAckReasonCode.GRANTED_QOS_2), 20 | INVALID(3, SubscribeAckReasonCode.IMPLEMENTATION_SPECIFIC_ERROR); 21 | 22 | private static final NumberedEnumMap NUMBERED_MAP = 23 | new NumberedEnumMap<>(QoS.class); 24 | 25 | public static QoS ofCode(int level) { 26 | return NUMBERED_MAP.resolve(level, QoS.INVALID); 27 | } 28 | 29 | int level; 30 | SubscribeAckReasonCode subscribeAckReasonCode; 31 | 32 | @Override 33 | public int number() { 34 | return level; 35 | } 36 | 37 | public QoS lower(QoS alternative) { 38 | return level > alternative.level ? alternative : this; 39 | } 40 | 41 | public boolean isLowerThan(QoS another) { 42 | return level < another.level; 43 | } 44 | 45 | public boolean isHigherThan(QoS another) { 46 | return level > another.level; 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /acl-groovy-dsl/src/main/groovy/javasabr/mqtt/acl/groovy/dsl/builder/RuleBuilder.groovy: -------------------------------------------------------------------------------- 1 | //file:noinspection unused 2 | package javasabr.mqtt.acl.groovy.dsl.builder 3 | 4 | import javasabr.mqtt.acl.engine.builder.TopicMatcherBuilder 5 | import javasabr.mqtt.acl.engine.exception.AclConfigurationException 6 | import javasabr.mqtt.acl.engine.model.Action 7 | import javasabr.mqtt.acl.engine.model.condition.MqttUserCondition 8 | import javasabr.mqtt.acl.engine.model.matcher.ValueMatcher 9 | import javasabr.mqtt.acl.engine.model.rule.Rule 10 | import javasabr.mqtt.model.acl.Operation 11 | 12 | abstract class RuleBuilder implements TopicMatcherBuilder { 13 | Action action 14 | Operation operation 15 | MqttUserCondition userCondition 16 | 17 | RuleBuilder(Action action, Operation operation) { 18 | this.action = action 19 | this.operation = operation 20 | } 21 | 22 | RuleBuilder allOf(Closure config) { 23 | if (this.userCondition) { 24 | throw new AclConfigurationException("Only one clients section allowed") 25 | } 26 | this.userCondition = new AllOfBuilder().buildCondition(config).build() 27 | return this 28 | } 29 | 30 | RuleBuilder anyOf(Closure config) { 31 | if (this.userCondition) { 32 | throw new AclConfigurationException("Only one clients section allowed") 33 | } 34 | this.userCondition = config == null ? MqttUserCondition.MATCH_ANY : new AnyOfBuilder().buildCondition(config).build() 35 | return this 36 | } 37 | 38 | static ValueMatcher anyone() { 39 | return ValueMatcher.MATCH_ANY 40 | } 41 | 42 | abstract Rule build() 43 | } 44 | -------------------------------------------------------------------------------- /network/src/test/groovy/javasabr/mqtt/network/message/out/UnsubscribeAckMqtt311OutMessageTest.groovy: -------------------------------------------------------------------------------- 1 | package javasabr.mqtt.network.message.out 2 | 3 | import javasabr.mqtt.model.message.MqttMessageType 4 | import javasabr.mqtt.network.message.in.MqttInMessage 5 | import javasabr.mqtt.network.message.in.UnsubscribeAckMqttInMessage 6 | import javasabr.rlib.common.util.BufferUtils 7 | import javasabr.rlib.common.util.NumberUtils 8 | 9 | class UnsubscribeAckMqtt311OutMessageTest extends BaseMqttOutMessageTest { 10 | 11 | def "should write message correctly"() { 12 | given: 13 | def outMessage = new UnsubscribeAckMqtt311OutMessage(testMessageId) 14 | when: 15 | def typeAndFlags = outMessage.messageTypeAndFlags() 16 | byte type = NumberUtils.getHighByteBits(typeAndFlags); 17 | byte info = NumberUtils.getLowByteBits(typeAndFlags); 18 | then: 19 | MqttMessageType.fromByte(type) == MqttMessageType.UNSUBSCRIBE_ACK 20 | info == UnsubscribeAckMqttInMessage.MESSAGE_FLAGS 21 | when: 22 | def dataBuffer = BufferUtils.prepareBuffer(512) { 23 | outMessage.write(defaultMqtt311Connection, it) 24 | } 25 | def reader = new UnsubscribeAckMqttInMessage(info) 26 | def result = reader.read(defaultMqtt311Connection, dataBuffer, dataBuffer.limit()) 27 | then: 28 | result 29 | reader.reasonCodes() == UnsubscribeAckMqttInMessage.EMPTY_REASON_CODES 30 | reader.messageId() == testMessageId 31 | reader.userProperties() == MqttInMessage.EMPTY_USER_PROPERTIES 32 | reader.reason() == null 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /network/src/test/groovy/javasabr/mqtt/network/message/out/PublishAckMqtt311OutMessageTest.groovy: -------------------------------------------------------------------------------- 1 | package javasabr.mqtt.network.message.out 2 | 3 | import javasabr.mqtt.model.message.MqttMessageType 4 | import javasabr.mqtt.model.reason.code.PublishAckReasonCode 5 | import javasabr.mqtt.network.message.in.PublishAckMqttInMessage 6 | import javasabr.rlib.common.util.BufferUtils 7 | import javasabr.rlib.common.util.NumberUtils 8 | 9 | class PublishAckMqtt311OutMessageTest extends BaseMqttOutMessageTest { 10 | 11 | def "should write message correctly"() { 12 | given: 13 | def outMessage = new PublishAckMqtt311OutMessage(testMessageId) 14 | when: 15 | def typeAndFlags = outMessage.messageTypeAndFlags() 16 | byte type = NumberUtils.getHighByteBits(typeAndFlags); 17 | byte info = NumberUtils.getLowByteBits(typeAndFlags); 18 | then: 19 | MqttMessageType.fromByte(type) == MqttMessageType.PUBLISH_ACK 20 | info == PublishAckMqttInMessage.MESSAGE_FLAGS 21 | when: 22 | def dataBuffer = BufferUtils.prepareBuffer(512) { 23 | outMessage.write(defaultMqtt311Connection, it) 24 | } 25 | def reader = new PublishAckMqttInMessage(info) 26 | def result = reader.read(defaultMqtt311Connection, dataBuffer, dataBuffer.limit()) 27 | then: 28 | result 29 | with(reader) { 30 | exception() == null 31 | reasonCode() == PublishAckReasonCode.SUCCESS 32 | messageId() == testMessageId 33 | userProperties() == MqttOutMessage.EMPTY_USER_PROPERTIES 34 | reason() == null 35 | } 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /network/src/test/groovy/javasabr/mqtt/network/message/out/PublishReleaseMqtt311OutMessageTest.groovy: -------------------------------------------------------------------------------- 1 | package javasabr.mqtt.network.message.out 2 | 3 | import javasabr.mqtt.model.message.MqttMessageType 4 | import javasabr.mqtt.model.reason.code.PublishReleaseReasonCode 5 | import javasabr.mqtt.network.message.in.PublishReleaseMqttInMessage 6 | import javasabr.rlib.common.util.BufferUtils 7 | import javasabr.rlib.common.util.NumberUtils 8 | 9 | class PublishReleaseMqtt311OutMessageTest extends BaseMqttOutMessageTest { 10 | 11 | def "should write message correctly"() { 12 | given: 13 | def outMessage = new PublishReleaseMqtt311OutMessage(testMessageId) 14 | when: 15 | def typeAndFlags = outMessage.messageTypeAndFlags() 16 | byte type = NumberUtils.getHighByteBits(typeAndFlags); 17 | byte info = NumberUtils.getLowByteBits(typeAndFlags); 18 | then: 19 | MqttMessageType.fromByte(type) == MqttMessageType.PUBLISH_RELEASE 20 | info == PublishReleaseMqttInMessage.MESSAGE_FLAGS 21 | when: 22 | def dataBuffer = BufferUtils.prepareBuffer(512) { 23 | outMessage.write(defaultMqtt311Connection, it) 24 | } 25 | def reader = new PublishReleaseMqttInMessage(info) 26 | def result = reader.read(defaultMqtt311Connection, dataBuffer, dataBuffer.limit()) 27 | then: 28 | result 29 | with(reader) { 30 | reasonCode() == PublishReleaseReasonCode.SUCCESS 31 | messageId() == testMessageId 32 | userProperties() == MqttOutMessage.EMPTY_USER_PROPERTIES 33 | reason() == null 34 | } 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /network/src/test/groovy/javasabr/mqtt/network/message/out/PublishCompleteMqtt311OutMessageTest.groovy: -------------------------------------------------------------------------------- 1 | package javasabr.mqtt.network.message.out 2 | 3 | import javasabr.mqtt.model.message.MqttMessageType 4 | import javasabr.mqtt.model.reason.code.PublishCompletedReasonCode 5 | import javasabr.mqtt.network.message.in.PublishCompleteMqttInMessage 6 | import javasabr.rlib.common.util.BufferUtils 7 | import javasabr.rlib.common.util.NumberUtils 8 | 9 | class PublishCompleteMqtt311OutMessageTest extends BaseMqttOutMessageTest { 10 | 11 | def "should write message correctly"() { 12 | given: 13 | def outMessage = new PublishCompleteMqtt311OutMessage(testMessageId) 14 | when: 15 | def typeAndFlags = outMessage.messageTypeAndFlags() 16 | byte type = NumberUtils.getHighByteBits(typeAndFlags); 17 | byte info = NumberUtils.getLowByteBits(typeAndFlags); 18 | then: 19 | MqttMessageType.fromByte(type) == MqttMessageType.PUBLISH_COMPLETE 20 | info == PublishCompleteMqttInMessage.MESSAGE_FLAGS 21 | when: 22 | def dataBuffer = BufferUtils.prepareBuffer(512) { 23 | outMessage.write(defaultMqtt311Connection, it) 24 | } 25 | def reader = new PublishCompleteMqttInMessage(info) 26 | def result = reader.read(defaultMqtt311Connection, dataBuffer, dataBuffer.limit()) 27 | then: 28 | result 29 | with(reader) { 30 | reasonCode() == PublishCompletedReasonCode.SUCCESS 31 | messageId() == testMessageId 32 | userProperties() == MqttOutMessage.EMPTY_USER_PROPERTIES 33 | reason() == null 34 | } 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /network/src/test/groovy/javasabr/mqtt/network/message/out/SubscribeAckMqtt311OutMessageTest.groovy: -------------------------------------------------------------------------------- 1 | package javasabr.mqtt.network.message.out 2 | 3 | import javasabr.mqtt.model.message.MqttMessageType 4 | import javasabr.mqtt.network.message.in.MqttInMessage 5 | import javasabr.mqtt.network.message.in.SubscribeAckMqttInMessage 6 | import javasabr.rlib.common.util.BufferUtils 7 | import javasabr.rlib.common.util.NumberUtils 8 | 9 | class SubscribeAckMqtt311OutMessageTest extends BaseMqttOutMessageTest { 10 | 11 | def "should write message correctly"() { 12 | given: 13 | def outMessage = new SubscribeAckMqtt311OutMessage(testMessageId, subscribeAckReasonCodes) 14 | when: 15 | def typeAndFlags = outMessage.messageTypeAndFlags() 16 | byte type = NumberUtils.getHighByteBits(typeAndFlags); 17 | byte info = NumberUtils.getLowByteBits(typeAndFlags); 18 | then: 19 | MqttMessageType.fromByte(type) == MqttMessageType.SUBSCRIBE_ACK 20 | info == SubscribeAckMqttInMessage.MESSAGE_FLAGS 21 | when: 22 | def dataBuffer = BufferUtils.prepareBuffer(512) { 23 | outMessage.write(defaultMqtt311Connection, it) 24 | } 25 | def reader = new SubscribeAckMqttInMessage(info) 26 | def result = reader.read(defaultMqtt311Connection, dataBuffer, dataBuffer.limit()) 27 | then: 28 | result 29 | reader.exception() == null 30 | reader.reasonCodes() == subscribeAckReasonCodes 31 | reader.messageId() == testMessageId 32 | reader.userProperties() == MqttInMessage.EMPTY_USER_PROPERTIES 33 | reader.reason() == null 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /network/src/test/groovy/javasabr/mqtt/network/message/out/PublishReceivedMqtt311OutMessageTest.groovy: -------------------------------------------------------------------------------- 1 | package javasabr.mqtt.network.message.out 2 | 3 | 4 | import javasabr.mqtt.model.message.MqttMessageType 5 | import javasabr.mqtt.model.reason.code.PublishReceivedReasonCode 6 | import javasabr.mqtt.network.message.in.PublishReceivedMqttInMessage 7 | import javasabr.rlib.common.util.BufferUtils 8 | import javasabr.rlib.common.util.NumberUtils 9 | 10 | class PublishReceivedMqtt311OutMessageTest extends BaseMqttOutMessageTest { 11 | 12 | def "should write message correctly"() { 13 | given: 14 | def outMessage = new PublishReceivedMqtt311OutMessage(testMessageId) 15 | when: 16 | def typeAndFlags = outMessage.messageTypeAndFlags() 17 | byte type = NumberUtils.getHighByteBits(typeAndFlags); 18 | byte info = NumberUtils.getLowByteBits(typeAndFlags); 19 | then: 20 | MqttMessageType.fromByte(type) == MqttMessageType.PUBLISH_RECEIVED 21 | info == PublishReceivedMqttInMessage.MESSAGE_FLAGS 22 | when: 23 | def dataBuffer = BufferUtils.prepareBuffer(512) { 24 | outMessage.write(defaultMqtt311Connection, it) 25 | } 26 | def reader = new PublishReceivedMqttInMessage(info) 27 | def result = reader.read(defaultMqtt311Connection, dataBuffer, dataBuffer.limit()) 28 | then: 29 | result 30 | with(reader) { 31 | reasonCode() == PublishReceivedReasonCode.SUCCESS 32 | messageId() == testMessageId 33 | userProperties() == MqttOutMessage.EMPTY_USER_PROPERTIES 34 | reason() == null 35 | } 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /network/src/test/groovy/javasabr/mqtt/network/message/out/UnsubscribeAckMqtt5OutMessageTest.groovy: -------------------------------------------------------------------------------- 1 | package javasabr.mqtt.network.message.out 2 | 3 | import javasabr.mqtt.model.message.MqttMessageType 4 | import javasabr.mqtt.network.message.in.UnsubscribeAckMqttInMessage 5 | import javasabr.rlib.common.util.BufferUtils 6 | import javasabr.rlib.common.util.NumberUtils 7 | 8 | class UnsubscribeAckMqtt5OutMessageTest extends BaseMqttOutMessageTest { 9 | 10 | def "should write message correctly"() { 11 | given: 12 | def outMessage = new UnsubscribeAckMqtt5OutMessage( 13 | testMessageId, 14 | unsubscribeAckReasonCodes, 15 | testUserProperties, 16 | reasonString) 17 | when: 18 | def typeAndFlags = outMessage.messageTypeAndFlags() 19 | byte type = NumberUtils.getHighByteBits(typeAndFlags); 20 | byte info = NumberUtils.getLowByteBits(typeAndFlags); 21 | then: 22 | MqttMessageType.fromByte(type) == MqttMessageType.UNSUBSCRIBE_ACK 23 | info == UnsubscribeAckMqttInMessage.MESSAGE_FLAGS 24 | when: 25 | def dataBuffer = BufferUtils.prepareBuffer(512) { 26 | outMessage.write(defaultMqtt5Connection, it) 27 | } 28 | def reader = new UnsubscribeAckMqttInMessage(info) 29 | def result = reader.read(defaultMqtt5Connection, dataBuffer, dataBuffer.limit()) 30 | then: 31 | result 32 | reader.reasonCodes() == unsubscribeAckReasonCodes 33 | reader.messageId() == testMessageId 34 | reader.userProperties() == testUserProperties 35 | reader.reason() == reasonString 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /acl-groovy-dsl/src/main/groovy/javasabr/mqtt/acl/groovy/dsl/loader/AclRulesLoader.groovy: -------------------------------------------------------------------------------- 1 | package javasabr.mqtt.acl.groovy.dsl.loader 2 | 3 | import javasabr.mqtt.acl.engine.builder.RuleContainerBuilder 4 | import javasabr.mqtt.acl.engine.exception.AclConfigurationException 5 | import javasabr.mqtt.acl.engine.model.rule.Rule 6 | import javasabr.mqtt.acl.groovy.dsl.builder.AclRulesBuilder 7 | import javasabr.mqtt.model.acl.Operation 8 | import javasabr.rlib.collections.array.Array 9 | import org.codehaus.groovy.control.CompilerConfiguration 10 | 11 | import java.nio.file.Files 12 | import java.nio.file.Path 13 | 14 | class AclRulesLoader { 15 | 16 | static Map> load(String aclConfigPath) { 17 | return load(Path.of(aclConfigPath)) 18 | } 19 | 20 | static Map> load(Path aclConfigPath) { 21 | if (Files.notExists(aclConfigPath)) { 22 | throw new AclConfigurationException("Config file:[%s] doesn't exist".formatted(aclConfigPath)) 23 | } 24 | CompilerConfiguration compilerConfig = new CompilerConfiguration() 25 | AclRulesBuilder aclRulesBuilder = new AclRulesBuilder() 26 | new GroovyShell(compilerConfig).with { 27 | setVariable("allowPublish", aclRulesBuilder.&allowPublish) 28 | setVariable("denyPublish", aclRulesBuilder.&denyPublish) 29 | setVariable("allowSubscribe", aclRulesBuilder.&allowSubscribe) 30 | setVariable("denySubscribe", aclRulesBuilder.&denySubscribe) 31 | evaluate(aclConfigPath.toFile()) 32 | } 33 | def allDefinedRules = aclRulesBuilder.build() 34 | return RuleContainerBuilder.groupRulesByOperation(allDefinedRules) 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /network/src/test/groovy/javasabr/mqtt/network/message/out/SubscribeAckMqtt5OutMessageTest.groovy: -------------------------------------------------------------------------------- 1 | package javasabr.mqtt.network.message.out 2 | 3 | import javasabr.mqtt.model.message.MqttMessageType 4 | import javasabr.mqtt.network.message.in.SubscribeAckMqttInMessage 5 | import javasabr.rlib.common.util.BufferUtils 6 | import javasabr.rlib.common.util.NumberUtils 7 | 8 | class SubscribeAckMqtt5OutMessageTest extends BaseMqttOutMessageTest { 9 | 10 | def "should write message correctly"() { 11 | given: 12 | def outMessage = new SubscribeAckMqtt5OutMessage( 13 | testMessageId, 14 | subscribeAckReasonCodes, 15 | testUserProperties, 16 | reasonString) 17 | when: 18 | def typeAndFlags = outMessage.messageTypeAndFlags() 19 | byte type = NumberUtils.getHighByteBits(typeAndFlags); 20 | byte info = NumberUtils.getLowByteBits(typeAndFlags); 21 | then: 22 | MqttMessageType.fromByte(type) == MqttMessageType.SUBSCRIBE_ACK 23 | info == SubscribeAckMqttInMessage.MESSAGE_FLAGS 24 | when: 25 | def dataBuffer = BufferUtils.prepareBuffer(512) { 26 | outMessage.write(defaultMqtt5Connection, it) 27 | } 28 | def reader = new SubscribeAckMqttInMessage(info) 29 | def result = reader.read(defaultMqtt5Connection, dataBuffer, dataBuffer.limit()) 30 | then: 31 | result 32 | reader.exception() == null 33 | reader.reasonCodes() == subscribeAckReasonCodes 34 | reader.messageId() == testMessageId 35 | reader.userProperties() == testUserProperties 36 | reader.reason() == reasonString 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /core-service/src/main/java/javasabr/mqtt/service/message/handler/impl/DisconnectMqttInMessageHandler.java: -------------------------------------------------------------------------------- 1 | package javasabr.mqtt.service.message.handler.impl; 2 | 3 | import javasabr.mqtt.model.message.MqttMessageType; 4 | import javasabr.mqtt.model.reason.code.DisconnectReasonCode; 5 | import javasabr.mqtt.network.MqttConnection; 6 | import javasabr.mqtt.network.impl.ExternalNetworkMqttUser; 7 | import javasabr.mqtt.network.message.in.DisconnectMqttInMessage; 8 | import javasabr.mqtt.network.session.NetworkMqttSession; 9 | import javasabr.mqtt.service.MessageOutFactoryService; 10 | import lombok.CustomLog; 11 | 12 | @CustomLog 13 | public class DisconnectMqttInMessageHandler extends AbstractMqttInMessageHandler { 14 | 15 | public DisconnectMqttInMessageHandler(MessageOutFactoryService messageOutFactoryService) { 16 | super(ExternalNetworkMqttUser.class, DisconnectMqttInMessage.class, messageOutFactoryService); 17 | } 18 | 19 | @Override 20 | public MqttMessageType messageType() { 21 | return MqttMessageType.DISCONNECT; 22 | } 23 | 24 | @Override 25 | protected void processValidMessage( 26 | MqttConnection connection, 27 | ExternalNetworkMqttUser user, 28 | NetworkMqttSession session, 29 | DisconnectMqttInMessage message) { 30 | DisconnectReasonCode reasonCode = message.reasonCode(); 31 | if (reasonCode == DisconnectReasonCode.NORMAL_DISCONNECTION) { 32 | log.info(user.clientId(), "Disconnect client:[%s]"::formatted); 33 | } else { 34 | log.error("Disconnect client:[%s] by error reason:[%s]".formatted(user, reasonCode)); 35 | } 36 | connection.close(); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /core-service/src/main/java/javasabr/mqtt/service/impl/FileCredentialsSource.java: -------------------------------------------------------------------------------- 1 | package javasabr.mqtt.service.impl; 2 | 3 | import java.io.FileInputStream; 4 | import java.io.IOException; 5 | import java.net.URL; 6 | import java.nio.charset.StandardCharsets; 7 | import java.util.Properties; 8 | import javasabr.mqtt.model.exception.CredentialsSourceException; 9 | import javasabr.rlib.collections.dictionary.DictionaryCollectors; 10 | import javasabr.rlib.collections.dictionary.RefToRefDictionary; 11 | 12 | public class FileCredentialsSource extends AbstractCredentialSource { 13 | 14 | private final String fileName; 15 | 16 | public FileCredentialsSource(String fileName) { 17 | this.fileName = fileName; 18 | init(); 19 | } 20 | 21 | @Override 22 | void init() { 23 | URL credentialUrl = FileCredentialsSource.class 24 | .getClassLoader() 25 | .getResource(fileName); 26 | 27 | if (credentialUrl == null) { 28 | throw new CredentialsSourceException("Credentials file:[%s] could not be found".formatted(fileName)); 29 | } 30 | 31 | try { 32 | var credentialsProperties = new Properties(); 33 | credentialsProperties.load(new FileInputStream(credentialUrl.getPath())); 34 | 35 | RefToRefDictionary credentials = credentialsProperties 36 | .entrySet() 37 | .stream() 38 | .collect(DictionaryCollectors.toRefToRefDictionary( 39 | entry -> entry.getKey().toString(), 40 | entry -> entry.getValue().toString().getBytes(StandardCharsets.UTF_8))); 41 | 42 | putAll(credentials); 43 | } catch (IOException e) { 44 | throw new CredentialsSourceException(e); 45 | } 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /acl-groovy-dsl/src/main/groovy/javasabr/mqtt/acl/groovy/dsl/builder/AllOfBuilder.groovy: -------------------------------------------------------------------------------- 1 | package javasabr.mqtt.acl.groovy.dsl.builder 2 | 3 | import javasabr.mqtt.acl.engine.exception.AclConfigurationException 4 | import javasabr.mqtt.acl.engine.model.condition.AllOfCondition 5 | import javasabr.mqtt.acl.engine.model.condition.MqttUserCondition 6 | import javasabr.mqtt.acl.engine.model.matcher.ValueMatcher 7 | import javasabr.rlib.collections.array.Array 8 | 9 | class AllOfBuilder extends ConditionBuilder { 10 | 11 | enum Identity { 12 | USER_NAME, CLIENT_ID, IP_ADDRESS 13 | } 14 | 15 | private final Set alreadySetIdentities = new HashSet<>() 16 | 17 | @Override 18 | ConditionBuilder userName(ValueMatcher... userNames) { 19 | requireSingleMatcher(Identity.USER_NAME, userNames) 20 | return super.userName(userNames) 21 | } 22 | 23 | @Override 24 | ConditionBuilder clientId(ValueMatcher... clientIds) { 25 | requireSingleMatcher(Identity.CLIENT_ID, clientIds) 26 | return super.clientId(clientIds) 27 | } 28 | 29 | ConditionBuilder ipAddress(ValueMatcher... ipAddresses) { 30 | requireSingleMatcher(Identity.IP_ADDRESS, ipAddresses) 31 | return super.ipAddress(ipAddresses) 32 | } 33 | 34 | private void requireSingleMatcher(Identity identity, ValueMatcher[] newMatchers) { 35 | if (alreadySetIdentities.contains(identity) || newMatchers.length > 1) { 36 | throw new AclConfigurationException("AllOf condition can only have single-matcher members") 37 | } else { 38 | alreadySetIdentities.add(identity) 39 | } 40 | } 41 | 42 | MqttUserCondition build() { 43 | return new AllOfCondition(Array.copyOf(conditions)) 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /network/src/test/groovy/javasabr/mqtt/network/message/out/PublishAckMqtt5OutMessageTest.groovy: -------------------------------------------------------------------------------- 1 | package javasabr.mqtt.network.message.out 2 | 3 | import javasabr.mqtt.model.message.MqttMessageType 4 | import javasabr.mqtt.model.reason.code.PublishAckReasonCode 5 | import javasabr.mqtt.network.message.in.PublishAckMqttInMessage 6 | import javasabr.rlib.common.util.BufferUtils 7 | import javasabr.rlib.common.util.NumberUtils 8 | 9 | class PublishAckMqtt5OutMessageTest extends BaseMqttOutMessageTest { 10 | 11 | def "should write message correctly"() { 12 | given: 13 | def outMessage = new PublishAckMqtt5OutMessage( 14 | testMessageId, 15 | PublishAckReasonCode.NOT_AUTHORIZED, 16 | reasonString, 17 | testUserProperties) 18 | when: 19 | def typeAndFlags = outMessage.messageTypeAndFlags() 20 | byte type = NumberUtils.getHighByteBits(typeAndFlags); 21 | byte info = NumberUtils.getLowByteBits(typeAndFlags); 22 | then: 23 | MqttMessageType.fromByte(type) == MqttMessageType.PUBLISH_ACK 24 | info == PublishAckMqttInMessage.MESSAGE_FLAGS 25 | when: 26 | def dataBuffer = BufferUtils.prepareBuffer(512) { 27 | outMessage.write(defaultMqtt5Connection, it) 28 | } 29 | def reader = new PublishAckMqttInMessage(info) 30 | def result = reader.read(defaultMqtt5Connection, dataBuffer, dataBuffer.limit()) 31 | then: 32 | result 33 | with(reader) { 34 | exception() == null 35 | reasonCode() == PublishAckReasonCode.NOT_AUTHORIZED 36 | messageId() == testMessageId 37 | userProperties() == testUserProperties 38 | reason() == reasonString 39 | } 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /acl-groovy-dsl/src/main/groovy/javasabr/mqtt/acl/groovy/dsl/builder/ConditionBuilder.groovy: -------------------------------------------------------------------------------- 1 | //file:noinspection unused 2 | package javasabr.mqtt.acl.groovy.dsl.builder 3 | 4 | import javasabr.mqtt.acl.engine.builder.ClientMatcherBuilder 5 | import javasabr.mqtt.acl.engine.model.condition.ClientIdCondition 6 | import javasabr.mqtt.acl.engine.model.condition.IpAddressCondition 7 | import javasabr.mqtt.acl.engine.model.condition.MqttUserCondition 8 | import javasabr.mqtt.acl.engine.model.condition.UserNameCondition 9 | import javasabr.mqtt.acl.engine.model.matcher.ValueMatcher 10 | import javasabr.rlib.collections.array.ArrayFactory 11 | import javasabr.rlib.collections.array.MutableArray 12 | 13 | abstract class ConditionBuilder implements ClientMatcherBuilder { 14 | 15 | protected MutableArray conditions = ArrayFactory.mutableArray(MqttUserCondition) 16 | 17 | ConditionBuilder userName(ValueMatcher... userNames) { 18 | def collect = userNames.collect { new UserNameCondition(it) } 19 | this.conditions.addAll(collect) 20 | return this 21 | } 22 | 23 | ConditionBuilder clientId(ValueMatcher... clientIds) { 24 | def collect = clientIds.collect { new ClientIdCondition(it) } 25 | this.conditions.addAll(collect) 26 | return this 27 | } 28 | 29 | ConditionBuilder ipAddress(ValueMatcher... ipAddresses) { 30 | def collect = ipAddresses.collect { new IpAddressCondition(it) } 31 | this.conditions.addAll(collect) 32 | return this 33 | } 34 | 35 | ConditionBuilder buildCondition(Closure config) { 36 | config.delegate = this 37 | config.resolveStrategy = Closure.DELEGATE_ONLY 38 | config() 39 | return this 40 | } 41 | 42 | abstract MqttUserCondition build() 43 | } 44 | -------------------------------------------------------------------------------- /application/src/test/groovy/javasabr/mqtt/broker/application/config/MqttBrokerTestConfig.groovy: -------------------------------------------------------------------------------- 1 | package javasabr.mqtt.broker.application.config 2 | 3 | import javasabr.mqtt.network.MqttConnection 4 | import javasabr.mqtt.service.ConnectionService 5 | import javasabr.rlib.network.server.ServerNetwork 6 | import org.springframework.boot.context.event.ApplicationStartedEvent 7 | import org.springframework.context.ApplicationListener 8 | import org.springframework.context.annotation.Bean 9 | import org.springframework.context.annotation.Configuration 10 | import org.springframework.context.annotation.Import 11 | 12 | import java.util.concurrent.ThreadLocalRandom 13 | 14 | @Import([ 15 | MqttBrokerSpringConfig, 16 | ]) 17 | @Configuration(proxyBeanMethods = false) 18 | class MqttBrokerTestConfig { 19 | 20 | @Bean 21 | InetSocketAddress externalNetworkAddress(ServerNetwork externalNetwork) { 22 | def random = ThreadLocalRandom.current() 23 | for (int i = 0; i < 100; i++) { 24 | def address = new InetSocketAddress("localhost", random.nextInt(800, 45000)) 25 | try { 26 | externalNetwork.start(address) 27 | return address; 28 | } catch (RuntimeException e) { 29 | } 30 | } 31 | throw new RuntimeException() 32 | } 33 | 34 | @Bean 35 | ApplicationListener externalNetworkStarter() { 36 | return (event) -> { }; 37 | } 38 | 39 | @Bean 40 | Void startExternalNetwork(ServerNetwork externalNetwork, 41 | ConnectionService connectionService, 42 | InetSocketAddress externalNetworkAddress) { 43 | externalNetwork.onAccept(connectionService::processAcceptedConnection); 44 | return null; 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /network/src/test/groovy/javasabr/mqtt/network/message/out/PublishReceivedMqtt5OutMessageTest.groovy: -------------------------------------------------------------------------------- 1 | package javasabr.mqtt.network.message.out 2 | 3 | import javasabr.mqtt.model.message.MqttMessageType 4 | import javasabr.mqtt.model.reason.code.PublishReceivedReasonCode 5 | import javasabr.mqtt.network.message.in.PublishReceivedMqttInMessage 6 | import javasabr.rlib.common.util.BufferUtils 7 | import javasabr.rlib.common.util.NumberUtils 8 | 9 | class PublishReceivedMqtt5OutMessageTest extends BaseMqttOutMessageTest { 10 | 11 | def "should write message correctly"() { 12 | given: 13 | def outMessage = new PublishReceivedMqtt5OutMessage( 14 | testMessageId, 15 | PublishReceivedReasonCode.UNSPECIFIED_ERROR, 16 | testUserProperties, 17 | reasonString) 18 | when: 19 | def typeAndFlags = outMessage.messageTypeAndFlags() 20 | byte type = NumberUtils.getHighByteBits(typeAndFlags); 21 | byte info = NumberUtils.getLowByteBits(typeAndFlags); 22 | then: 23 | MqttMessageType.fromByte(type) == MqttMessageType.PUBLISH_RECEIVED 24 | info == PublishReceivedMqttInMessage.MESSAGE_FLAGS 25 | when: 26 | def dataBuffer = BufferUtils.prepareBuffer(512) { 27 | outMessage.write(defaultMqtt5Connection, it) 28 | } 29 | def reader = new PublishReceivedMqttInMessage(info) 30 | def result = reader.read(defaultMqtt5Connection, dataBuffer, dataBuffer.limit()) 31 | then: 32 | result 33 | with(reader) { 34 | reasonCode() == PublishReceivedReasonCode.UNSPECIFIED_ERROR 35 | messageId() == testMessageId 36 | userProperties() == testUserProperties 37 | reason() == reasonString 38 | } 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /acl-engine/src/main/java/javasabr/mqtt/acl/engine/builder/RuleContainerBuilder.java: -------------------------------------------------------------------------------- 1 | package javasabr.mqtt.acl.engine.builder; 2 | 3 | import java.util.Collections; 4 | import java.util.EnumMap; 5 | import java.util.Map; 6 | import javasabr.mqtt.acl.engine.model.rule.Rule; 7 | import javasabr.mqtt.model.acl.Operation; 8 | import javasabr.rlib.collections.array.Array; 9 | import javasabr.rlib.collections.array.ArrayFactory; 10 | import javasabr.rlib.collections.array.MutableArray; 11 | import lombok.AccessLevel; 12 | import lombok.NoArgsConstructor; 13 | 14 | @NoArgsConstructor(access = AccessLevel.PRIVATE) 15 | public final class RuleContainerBuilder { 16 | 17 | private static final Array EMPTY_RULES = Array.empty(Rule.class); 18 | 19 | public static Map> groupRulesByOperation(Array rules) { 20 | var intermediate = new EnumMap>(Operation.class); 21 | for (Rule rule : rules) { 22 | intermediate 23 | .computeIfAbsent(rule.operation(), RuleContainerBuilder::newMutableArray) 24 | .add(rule); 25 | } 26 | var finalMap = new EnumMap>(Operation.class); 27 | for (var entry : intermediate.entrySet()) { 28 | finalMap.put(entry.getKey(), Array.copyOf(entry.getValue())); 29 | } 30 | Operation.forEach(operation -> { 31 | finalMap.computeIfAbsent(operation, RuleContainerBuilder::emptyArray); 32 | }); 33 | // no need to wrap this map because it's not public API 34 | return finalMap; 35 | } 36 | 37 | static Array emptyArray(Operation ignored) { 38 | return EMPTY_RULES; 39 | } 40 | 41 | static MutableArray newMutableArray(Operation ignored) { 42 | return ArrayFactory.mutableArray(Rule.class); 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /model/src/main/java/javasabr/mqtt/model/subscription/RequestedSubscription.java: -------------------------------------------------------------------------------- 1 | package javasabr.mqtt.model.subscription; 2 | 3 | import javasabr.mqtt.model.QoS; 4 | import javasabr.mqtt.model.SubscribeRetainHandling; 5 | 6 | public record RequestedSubscription( 7 | /* 8 | The subscriber's topic filter. 9 | */ 10 | String rawTopicFilter, 11 | /* 12 | Maximum QoS field. This gives the maximum QoS level at which the Server can send Application Messages to the 13 | Client. 14 | */ 15 | QoS qos, 16 | /* 17 | This option specifies whether retained messages are sent when the subscription is established. This does not affect 18 | the sending of retained messages at any point after the subscribe. If there are no retained messages matching the 19 | Topic Filter, all of these values act the same. 20 | */ 21 | SubscribeRetainHandling retainHandling, 22 | /* 23 | If the value is true, Application Messages MUST NOT be forwarded to a connection with a ClientID equal to the 24 | ClientID of the publishing connection. 25 | */ 26 | boolean noLocal, 27 | /* 28 | If true, Application Messages forwarded using this subscription keep the RETAIN flag they were published with. If 29 | false, Application Messages forwarded using this subscription have the RETAIN flag set to 0. Retained messages sent 30 | when the subscription is established have the RETAIN flag set to 1. 31 | */ 32 | boolean retainAsPublished) { 33 | 34 | public static RequestedSubscription minimal(String rawTopicFilter, QoS qos) { 35 | return new RequestedSubscription( 36 | rawTopicFilter, 37 | qos, 38 | SubscribeRetainHandling.SEND, 39 | true, 40 | true); 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /network/src/test/groovy/javasabr/mqtt/network/message/out/PublishReleaseMqtt5OutMessageTest.groovy: -------------------------------------------------------------------------------- 1 | package javasabr.mqtt.network.message.out 2 | 3 | import javasabr.mqtt.model.message.MqttMessageType 4 | import javasabr.mqtt.model.reason.code.PublishReleaseReasonCode 5 | import javasabr.mqtt.network.message.in.PublishReleaseMqttInMessage 6 | import javasabr.rlib.common.util.BufferUtils 7 | import javasabr.rlib.common.util.NumberUtils 8 | 9 | class PublishReleaseMqtt5OutMessageTest extends BaseMqttOutMessageTest { 10 | 11 | def "should write message correctly"() { 12 | given: 13 | def outMessage = new PublishReleaseMqtt5OutMessage( 14 | testMessageId, 15 | PublishReleaseReasonCode.PACKET_IDENTIFIER_NOT_FOUND, 16 | testUserProperties, 17 | reasonString) 18 | when: 19 | def typeAndFlags = outMessage.messageTypeAndFlags() 20 | byte type = NumberUtils.getHighByteBits(typeAndFlags); 21 | byte info = NumberUtils.getLowByteBits(typeAndFlags); 22 | then: 23 | MqttMessageType.fromByte(type) == MqttMessageType.PUBLISH_RELEASE 24 | info == PublishReleaseMqttInMessage.MESSAGE_FLAGS 25 | when: 26 | def dataBuffer = BufferUtils.prepareBuffer(512) { 27 | outMessage.write(defaultMqtt5Connection, it) 28 | } 29 | def reader = new PublishReleaseMqttInMessage(info) 30 | def result = reader.read(defaultMqtt5Connection, dataBuffer, dataBuffer.limit()) 31 | then: 32 | result 33 | with(reader) { 34 | reasonCode() == PublishReleaseReasonCode.PACKET_IDENTIFIER_NOT_FOUND 35 | messageId() == testMessageId 36 | userProperties() == testUserProperties 37 | reason() == reasonString 38 | } 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /network/src/test/groovy/javasabr/mqtt/network/message/out/PublishCompleteMqtt5OutMessageTest.groovy: -------------------------------------------------------------------------------- 1 | package javasabr.mqtt.network.message.out 2 | 3 | import javasabr.mqtt.model.message.MqttMessageType 4 | import javasabr.mqtt.model.reason.code.PublishCompletedReasonCode 5 | import javasabr.mqtt.network.message.in.PublishCompleteMqttInMessage 6 | import javasabr.rlib.common.util.BufferUtils 7 | import javasabr.rlib.common.util.NumberUtils 8 | 9 | class PublishCompleteMqtt5OutMessageTest extends BaseMqttOutMessageTest { 10 | 11 | def "should write message correctly"() { 12 | given: 13 | def outMessage = new PublishCompleteMqtt5OutMessage( 14 | testMessageId, 15 | PublishCompletedReasonCode.PACKET_IDENTIFIER_NOT_FOUND, 16 | testUserProperties, 17 | reasonString) 18 | when: 19 | def typeAndFlags = outMessage.messageTypeAndFlags() 20 | byte type = NumberUtils.getHighByteBits(typeAndFlags); 21 | byte info = NumberUtils.getLowByteBits(typeAndFlags); 22 | then: 23 | MqttMessageType.fromByte(type) == MqttMessageType.PUBLISH_COMPLETE 24 | info == PublishCompleteMqttInMessage.MESSAGE_FLAGS 25 | when: 26 | def dataBuffer = BufferUtils.prepareBuffer(512) { 27 | outMessage.write(defaultMqtt5Connection, it) 28 | } 29 | def reader = new PublishCompleteMqttInMessage(info) 30 | def result = reader.read(defaultMqtt5Connection, dataBuffer, dataBuffer.limit()) 31 | then: 32 | result 33 | with(reader) { 34 | reasonCode() == PublishCompletedReasonCode.PACKET_IDENTIFIER_NOT_FOUND 35 | messageId() == testMessageId 36 | userProperties() == testUserProperties 37 | reason() == reasonString 38 | } 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /acl-groovy-dsl/src/main/groovy/javasabr/mqtt/acl/groovy/dsl/builder/AclRulesBuilder.groovy: -------------------------------------------------------------------------------- 1 | //file:noinspection unused 2 | package javasabr.mqtt.acl.groovy.dsl.builder 3 | 4 | import javasabr.mqtt.acl.engine.model.rule.Rule 5 | import javasabr.rlib.collections.array.Array 6 | import javasabr.rlib.collections.array.ArrayFactory 7 | 8 | import java.util.concurrent.CompletableFuture 9 | 10 | /** 11 | * Builds list of {@link Rule} from ACL configuration 12 | */ 13 | class AclRulesBuilder { 14 | 15 | private final List> ruleBuilderFutures = [] 16 | 17 | Array build() { 18 | return ruleBuilderFutures.collect(ArrayFactory.mutableArray(Rule.class), { it.join() }) 19 | } 20 | 21 | void allowPublish(Closure config) { 22 | ruleBuilderFutures.add(startBuilderAsync(new AllowPublishRuleBuilder(), config)) 23 | } 24 | 25 | void denyPublish(Closure config) { 26 | ruleBuilderFutures.add(startBuilderAsync(new DenyPublishRuleBuilder(), config)) 27 | } 28 | 29 | void allowSubscribe(Closure config) { 30 | ruleBuilderFutures.add(startBuilderAsync(new AllowSubscribeRuleBuilder(), config)) 31 | } 32 | 33 | void denySubscribe(Closure config) { 34 | ruleBuilderFutures.add(startBuilderAsync(new DenySubscribeRuleBuilder(), config)) 35 | } 36 | 37 | private static CompletableFuture startBuilderAsync(RuleBuilder builder, Closure config) { 38 | CompletableFuture.supplyAsync({ putConfigToBuilder(builder, config).build() }) 39 | } 40 | 41 | private static RuleBuilder putConfigToBuilder( 42 | RuleBuilder ruleBuilder, 43 | Closure ruleConfigurator) { 44 | ruleConfigurator.delegate = ruleBuilder 45 | ruleConfigurator.resolveStrategy = Closure.DELEGATE_ONLY 46 | ruleConfigurator() 47 | return ruleBuilder 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /model/src/main/java/javasabr/mqtt/model/subscriber/tree/ConcurrentSubscriberTree.java: -------------------------------------------------------------------------------- 1 | package javasabr.mqtt.model.subscriber.tree; 2 | 3 | import javasabr.mqtt.model.MqttUser; 4 | import javasabr.mqtt.model.subscriber.SingleSubscriber; 5 | import javasabr.mqtt.model.subscription.Subscription; 6 | import javasabr.mqtt.model.topic.TopicFilter; 7 | import javasabr.mqtt.model.topic.TopicName; 8 | import javasabr.rlib.collections.array.Array; 9 | import javasabr.rlib.collections.array.MutableArray; 10 | import javasabr.rlib.common.ThreadSafe; 11 | import lombok.AccessLevel; 12 | import lombok.experimental.FieldDefaults; 13 | import org.jspecify.annotations.Nullable; 14 | 15 | @FieldDefaults(level = AccessLevel.PRIVATE, makeFinal = true) 16 | public class ConcurrentSubscriberTree implements ThreadSafe { 17 | 18 | SubscriberNode rootNode; 19 | 20 | public ConcurrentSubscriberTree() { 21 | this.rootNode = new SubscriberNode(); 22 | } 23 | 24 | @Nullable 25 | public SingleSubscriber subscribe(MqttUser user, Subscription subscription) { 26 | return rootNode.subscribe(0, user, subscription, subscription.topicFilter()); 27 | } 28 | 29 | public boolean unsubscribe(MqttUser user, TopicFilter topicFilter) { 30 | return rootNode.unsubscribe(0, user, topicFilter); 31 | } 32 | 33 | public Array matches(TopicName topicName) { 34 | var resultArray = MutableArray.ofType(SingleSubscriber.class); 35 | matchesTo(resultArray, topicName); 36 | return resultArray; 37 | } 38 | 39 | public MutableArray matchesTo(MutableArray container, TopicName topicName) { 40 | var resultArray = MutableArray.ofType(SingleSubscriber.class); 41 | rootNode.matchesTo(0, topicName, topicName.levelsCount() - 1, container); 42 | return resultArray; 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /network/src/test/groovy/javasabr/mqtt/network/message/out/AuthenticationMqtt5OutMessageTest.groovy: -------------------------------------------------------------------------------- 1 | package javasabr.mqtt.network.message.out 2 | 3 | import javasabr.mqtt.model.message.MqttMessageType 4 | import javasabr.mqtt.model.reason.code.AuthenticateReasonCode 5 | import javasabr.mqtt.network.message.in.AuthenticationMqttInMessage 6 | import javasabr.rlib.common.util.BufferUtils 7 | import javasabr.rlib.common.util.NumberUtils 8 | 9 | class AuthenticationMqtt5OutMessageTest extends BaseMqttOutMessageTest { 10 | 11 | def "should write message correctly"() { 12 | given: 13 | def outMessage = new AuthenticationMqtt5OutMessage( 14 | AuthenticateReasonCode.CONTINUE_AUTHENTICATION, 15 | reasonString, 16 | authMethod, 17 | authData, 18 | testUserProperties) 19 | when: 20 | def typeAndFlags = outMessage.messageTypeAndFlags() 21 | byte type = NumberUtils.getHighByteBits(typeAndFlags); 22 | byte info = NumberUtils.getLowByteBits(typeAndFlags); 23 | then: 24 | MqttMessageType.fromByte(type) == MqttMessageType.AUTHENTICATION 25 | info == AuthenticationMqttInMessage.MESSAGE_FLAGS 26 | when: 27 | def dataBuffer = BufferUtils.prepareBuffer(512) { 28 | outMessage.write(defaultMqtt5Connection, it) 29 | } 30 | def reader = new AuthenticationMqttInMessage(info) 31 | def result = reader.read(defaultMqtt5Connection, dataBuffer, dataBuffer.limit()) 32 | then: 33 | result 34 | reader.reasonCode() == AuthenticateReasonCode.CONTINUE_AUTHENTICATION 35 | reader.reason() == reasonString 36 | reader.authenticationMethod() == authMethod 37 | reader.authenticationData() == authData 38 | reader.userProperties() == testUserProperties 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /model/src/main/java/javasabr/mqtt/model/reason/code/UnsubscribeAckReasonCode.java: -------------------------------------------------------------------------------- 1 | package javasabr.mqtt.model.reason.code; 2 | 3 | import javasabr.rlib.common.util.NumberedEnum; 4 | import javasabr.rlib.common.util.NumberedEnumMap; 5 | import lombok.Getter; 6 | import lombok.RequiredArgsConstructor; 7 | import lombok.experimental.Accessors; 8 | 9 | @Getter 10 | @Accessors 11 | @RequiredArgsConstructor 12 | public enum UnsubscribeAckReasonCode implements NumberedEnum, ReasonCode { 13 | /** 14 | * The subscription is deleted. 15 | */ 16 | SUCCESS(0x00), 17 | /** 18 | * The subscription is accepted and the maximum QoS sent will be QoS 1. This might be a lower QoS than was requested. 19 | */ 20 | NO_SUBSCRIPTION_EXISTED(0x11), 21 | 22 | // ERRORS 23 | /** 24 | * The unsubscribe could not be completed and the Server either does not wish to reveal the reason or none of the 25 | * other Reason Codes apply. 26 | */ 27 | UNSPECIFIED_ERROR(0x80), 28 | /** 29 | * The UNSUBSCRIBE is valid but the Server does not accept it. 30 | */ 31 | IMPLEMENTATION_SPECIFIC_ERROR(0x83), 32 | /** 33 | * The Client is not authorized to unsubscribe. 34 | */ 35 | NOT_AUTHORIZED(0x87), 36 | /** 37 | * The Topic Filter is correctly formed but is not allowed for this Client. 38 | */ 39 | TOPIC_FILTER_INVALID(0x8F), 40 | /** 41 | * The specified Packet Identifier is already in use. 42 | */ 43 | PACKET_IDENTIFIER_IN_USE(0x91); 44 | 45 | private static final NumberedEnumMap NUMBERED_MAP = 46 | new NumberedEnumMap<>(UnsubscribeAckReasonCode.class); 47 | 48 | public static UnsubscribeAckReasonCode ofCode(int code) { 49 | return NUMBERED_MAP.require(code); 50 | } 51 | 52 | private final int code; 53 | 54 | @Override 55 | public int number() { 56 | return code; 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /base/src/main/java/javasabr/mqtt/base/util/ReactorUtils.java: -------------------------------------------------------------------------------- 1 | package javasabr.mqtt.base.util; 2 | 3 | import java.util.function.BiFunction; 4 | import java.util.function.Consumer; 5 | import java.util.function.Function; 6 | import java.util.function.Supplier; 7 | import javasabr.rlib.common.util.StringUtils; 8 | import reactor.core.publisher.Mono; 9 | 10 | public class ReactorUtils { 11 | 12 | public static Function ifTrue(Runnable function) { 13 | return value -> { 14 | if (value) { 15 | function.run(); 16 | } 17 | return value; 18 | }; 19 | } 20 | 21 | public static Function ifTrue(A arg, Consumer function) { 22 | return value -> { 23 | if (value) { 24 | function.accept(arg); 25 | } 26 | return value; 27 | }; 28 | } 29 | 30 | public static Function> ifNotEmpty( 31 | Function> toContinue, 32 | Supplier> another) { 33 | return value -> { 34 | if (StringUtils.isNotEmpty(value)) { 35 | return toContinue.apply(value); 36 | } else { 37 | return another.get(); 38 | } 39 | }; 40 | } 41 | 42 | public static Function> ifTrue(Supplier> function, Runnable another) { 43 | return value -> { 44 | 45 | if (!value) { 46 | another.run(); 47 | return Mono.empty(); 48 | } 49 | 50 | return function.get(); 51 | }; 52 | } 53 | 54 | public static Function> ifTrue( 55 | T1 arg1, 56 | T2 arg2, 57 | BiFunction> function, 58 | T3 arg3, 59 | Consumer another) { 60 | return value -> { 61 | 62 | if (value) { 63 | return function.apply(arg1, arg2); 64 | } 65 | 66 | another.accept(arg3); 67 | return Mono.empty(); 68 | }; 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /application/src/test/groovy/javasabr/mqtt/broker/application/service/NetworkMqttSessionServiceTest.groovy: -------------------------------------------------------------------------------- 1 | package javasabr.mqtt.broker.application.service 2 | 3 | import com.hivemq.client.mqtt.mqtt5.message.connect.connack.Mqtt5ConnAckReasonCode 4 | import javasabr.mqtt.broker.application.IntegrationSpecification 5 | import javasabr.mqtt.service.ClientIdRegistry 6 | import javasabr.mqtt.service.session.MqttSessionService 7 | import org.springframework.beans.factory.annotation.Autowired 8 | 9 | class NetworkMqttSessionServiceTest extends IntegrationSpecification { 10 | 11 | @Autowired 12 | ClientIdRegistry clientIdRegistry 13 | 14 | @Autowired 15 | MqttSessionService mqttSessionService 16 | 17 | def "subscriber should create and re-use mqtt session"() { 18 | given: 19 | def clientId = clientIdRegistry.generate().block() 20 | def client = buildExternalMqtt5Client(clientId) 21 | when: 22 | def shouldNoSession = mqttSessionService.restore(clientId).block() 23 | def result = client.connect().join() 24 | then: 25 | result.reasonCode == Mqtt5ConnAckReasonCode.SUCCESS 26 | shouldNoSession == null 27 | mqttSessionService.restore(clientId).block() == null 28 | when: 29 | client.disconnect().join() 30 | Thread.sleep(100) 31 | def restored = mqttSessionService.restore(clientId).block() 32 | then: 33 | restored != null 34 | when: 35 | mqttSessionService.store(clientId, restored, externalConnectionConfig.defaultSessionExpiryInterval()).block() 36 | client.connect().join() 37 | shouldNoSession = mqttSessionService.restore(clientId).block() 38 | then: 39 | shouldNoSession == null 40 | when: 41 | client.disconnect().join() 42 | Thread.sleep(100) 43 | restored = mqttSessionService.restore(clientId).block() 44 | then: 45 | restored != null 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /network/src/main/java/javasabr/mqtt/network/message/out/SubscribeMqtt311OutMessage.java: -------------------------------------------------------------------------------- 1 | package javasabr.mqtt.network.message.out; 2 | 3 | import java.nio.ByteBuffer; 4 | import javasabr.mqtt.model.message.MqttMessageType; 5 | import javasabr.mqtt.model.subscription.Subscription; 6 | import javasabr.mqtt.model.topic.TopicFilter; 7 | import javasabr.mqtt.network.MqttConnection; 8 | import javasabr.rlib.collections.array.Array; 9 | import lombok.AccessLevel; 10 | import lombok.experimental.FieldDefaults; 11 | 12 | /** 13 | * Subscribe request. 14 | */ 15 | @FieldDefaults(level = AccessLevel.PROTECTED, makeFinal = true) 16 | public class SubscribeMqtt311OutMessage extends TrackableMqttOutMessage { 17 | 18 | private static final byte MESSAGE_TYPE = (byte) MqttMessageType.SUBSCRIBE.ordinal(); 19 | 20 | Array subscriptions; 21 | 22 | public SubscribeMqtt311OutMessage(int messageId, Array subscriptions) { 23 | super(messageId); 24 | this.subscriptions = subscriptions; 25 | } 26 | 27 | @Override 28 | protected byte messageTypeId() { 29 | return MESSAGE_TYPE; 30 | } 31 | 32 | @Override 33 | public MqttMessageType messageType() { 34 | return MqttMessageType.SUBSCRIBE; 35 | } 36 | 37 | @Override 38 | protected byte messageFlags() { 39 | return 0b0000_0010; 40 | } 41 | 42 | @Override 43 | protected void writePayload(MqttConnection connection, ByteBuffer buffer) { 44 | // http://docs.oasis-open.org/mqtt/mqtt/v3.1.1/os/mqtt-v3.1.1-os.html#_Toc398718066 45 | for (Subscription subscribedTopic : subscriptions) { 46 | TopicFilter topicFilter = subscribedTopic.topicFilter(); 47 | writeString(buffer, topicFilter.rawTopic()); 48 | writeByte(buffer, buildSubscriptionOptions(subscribedTopic)); 49 | } 50 | } 51 | 52 | protected int buildSubscriptionOptions(Subscription topicFilter) { 53 | return topicFilter.qos().level(); 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /core-service/src/main/java/javasabr/mqtt/service/message/validator/PublishRetainMqttInMessageFieldValidator.java: -------------------------------------------------------------------------------- 1 | package javasabr.mqtt.service.message.validator; 2 | 3 | import javasabr.mqtt.model.MqttClientConnectionConfig; 4 | import javasabr.mqtt.model.reason.code.DisconnectReasonCode; 5 | import javasabr.mqtt.network.MqttConnection; 6 | import javasabr.mqtt.network.message.in.PublishMqttInMessage; 7 | import javasabr.mqtt.network.user.NetworkMqttUser; 8 | import javasabr.mqtt.service.MessageOutFactoryService; 9 | import lombok.AccessLevel; 10 | import lombok.CustomLog; 11 | import lombok.RequiredArgsConstructor; 12 | import lombok.experimental.FieldDefaults; 13 | 14 | @CustomLog 15 | @RequiredArgsConstructor 16 | @FieldDefaults(level = AccessLevel.PRIVATE, makeFinal = true) 17 | public class PublishRetainMqttInMessageFieldValidator extends 18 | MqttInMessageFieldValidator { 19 | 20 | public static final int ORDER = PublishQosMqttInMessageFieldValidator.ORDER + 1; 21 | 22 | MessageOutFactoryService messageOutFactoryService; 23 | 24 | @Override 25 | public boolean isNotValid(MqttConnection connection, NetworkMqttUser user, PublishMqttInMessage message) { 26 | MqttClientConnectionConfig connectionConfig = connection.clientConnectionConfig(); 27 | boolean retain = message.retain(); 28 | if (retain && !connectionConfig.retainAvailable()) { 29 | log.warning(user.clientId(), "[%s] 'RETAIN' option is not supported"::formatted); 30 | handleNotSupportedRetain(user); 31 | return true; 32 | } 33 | return false; 34 | } 35 | 36 | @Override 37 | public int order() { 38 | return ORDER; 39 | } 40 | 41 | private void handleNotSupportedRetain(NetworkMqttUser user) { 42 | user.closeWithReason(messageOutFactoryService 43 | .resolveFactory(user) 44 | .newDisconnect(user, DisconnectReasonCode.RETAIN_NOT_SUPPORTED)); 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /core-service/src/main/java/javasabr/mqtt/service/SubscriptionService.java: -------------------------------------------------------------------------------- 1 | package javasabr.mqtt.service; 2 | 3 | import javasabr.mqtt.model.MqttUser; 4 | import javasabr.mqtt.model.reason.code.SubscribeAckReasonCode; 5 | import javasabr.mqtt.model.reason.code.UnsubscribeAckReasonCode; 6 | import javasabr.mqtt.model.session.MqttSession; 7 | import javasabr.mqtt.model.subscriber.SingleSubscriber; 8 | import javasabr.mqtt.model.subscription.Subscription; 9 | import javasabr.mqtt.model.topic.TopicFilter; 10 | import javasabr.mqtt.model.topic.TopicName; 11 | import javasabr.rlib.collections.array.Array; 12 | import javasabr.rlib.collections.array.MutableArray; 13 | 14 | /** 15 | * Subscription service 16 | */ 17 | public interface SubscriptionService { 18 | 19 | default Array findSubscribers(TopicName topicName) { 20 | return findSubscribersTo(MutableArray.ofType(SingleSubscriber.class), topicName); 21 | } 22 | 23 | Array findSubscribersTo(MutableArray container, TopicName topicName); 24 | 25 | /** 26 | * Subscribes MQTT client to listen to topics. 27 | * 28 | * @param user MQTT client which requests subscriptions 29 | * @param subscriptions the list of request to subscribe topics 30 | * @return array of subscribe ack reason codes 31 | */ 32 | Array subscribe(MqttUser user, MqttSession session, Array subscriptions); 33 | 34 | /** 35 | * Removes MQTT client from listening to the topics. 36 | * 37 | * @param user MQTT client to be removed 38 | * @param topicFilters topic filters 39 | * @return array of unsubscribe ack reason codes 40 | */ 41 | Array unsubscribe(MqttUser user, MqttSession session, Array topicFilters); 42 | 43 | void cleanSubscriptions(MqttUser user, MqttSession session); 44 | 45 | void restoreSubscriptions(MqttUser user, MqttSession session); 46 | } 47 | -------------------------------------------------------------------------------- /core-service/src/main/java/javasabr/mqtt/service/session/impl/InMemoryTopicNameMapping.java: -------------------------------------------------------------------------------- 1 | package javasabr.mqtt.service.session.impl; 2 | 3 | import java.util.concurrent.locks.StampedLock; 4 | import javasabr.mqtt.model.session.TopicNameMapping; 5 | import javasabr.mqtt.model.topic.TopicName; 6 | import javasabr.rlib.collections.dictionary.DictionaryFactory; 7 | import javasabr.rlib.collections.dictionary.MutableIntToRefDictionary; 8 | import lombok.AccessLevel; 9 | import lombok.experimental.FieldDefaults; 10 | import org.jspecify.annotations.Nullable; 11 | 12 | @FieldDefaults(level = AccessLevel.PRIVATE, makeFinal = true) 13 | public class InMemoryTopicNameMapping implements TopicNameMapping { 14 | 15 | MutableIntToRefDictionary topicNameAliases; 16 | StampedLock lock; 17 | 18 | public InMemoryTopicNameMapping() { 19 | this.topicNameAliases = DictionaryFactory.mutableIntToRefDictionary(); 20 | this.lock = new StampedLock(); 21 | } 22 | 23 | @Override 24 | public void update(int topicAlias, TopicName topicName) { 25 | long stamp = lock.readLock(); 26 | try { 27 | TopicName existed = topicNameAliases.get(topicAlias); 28 | if (existed != null && existed.equals(topicName)) { 29 | return; 30 | } 31 | } finally { 32 | lock.unlockRead(stamp); 33 | } 34 | stamp = lock.writeLock(); 35 | try { 36 | topicNameAliases.put(topicAlias, topicName); 37 | } finally { 38 | lock.unlockWrite(stamp); 39 | } 40 | } 41 | 42 | @Nullable 43 | @Override 44 | public TopicName resolve(int topicAlias) { 45 | long stamp = lock.readLock(); 46 | try { 47 | return topicNameAliases.get(topicAlias); 48 | } finally { 49 | lock.unlockRead(stamp); 50 | } 51 | } 52 | 53 | public void clear() { 54 | long stamp = lock.writeLock(); 55 | try { 56 | topicNameAliases.clear(); 57 | } finally { 58 | lock.unlockWrite(stamp); 59 | } 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /core-service/src/main/java/javasabr/mqtt/service/message/validator/PublishQosMqttInMessageFieldValidator.java: -------------------------------------------------------------------------------- 1 | package javasabr.mqtt.service.message.validator; 2 | 3 | import javasabr.mqtt.model.MqttClientConnectionConfig; 4 | import javasabr.mqtt.model.QoS; 5 | import javasabr.mqtt.model.reason.code.DisconnectReasonCode; 6 | import javasabr.mqtt.network.MqttConnection; 7 | import javasabr.mqtt.network.message.in.PublishMqttInMessage; 8 | import javasabr.mqtt.network.user.NetworkMqttUser; 9 | import javasabr.mqtt.service.MessageOutFactoryService; 10 | import lombok.AccessLevel; 11 | import lombok.CustomLog; 12 | import lombok.RequiredArgsConstructor; 13 | import lombok.experimental.FieldDefaults; 14 | 15 | @CustomLog 16 | @RequiredArgsConstructor 17 | @FieldDefaults(level = AccessLevel.PRIVATE, makeFinal = true) 18 | public class PublishQosMqttInMessageFieldValidator extends 19 | MqttInMessageFieldValidator { 20 | 21 | public static final int ORDER = PublishPayloadMqttInMessageFieldValidator.ORDER + 1; 22 | 23 | MessageOutFactoryService messageOutFactoryService; 24 | 25 | @Override 26 | public boolean isNotValid(MqttConnection connection, NetworkMqttUser user, PublishMqttInMessage message) { 27 | QoS requestedQos = message.qos(); 28 | MqttClientConnectionConfig connectionConfig = connection.clientConnectionConfig(); 29 | if (connectionConfig.maxQos().isLowerThan(requestedQos)) { 30 | log.warning(user.clientId(), requestedQos, "[%s] Requested QoS:[%s] is not supported"::formatted); 31 | handleNotSupportedQos(user); 32 | return true; 33 | } 34 | return false; 35 | } 36 | 37 | @Override 38 | public int order() { 39 | return ORDER; 40 | } 41 | 42 | private void handleNotSupportedQos(NetworkMqttUser user) { 43 | user.closeWithReason(messageOutFactoryService 44 | .resolveFactory(user) 45 | .newDisconnect(user, DisconnectReasonCode.QOS_NOT_SUPPORTED)); 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /core-service/src/test/groovy/javasabr/mqtt/service/publish/handler/impl/Qos0MqttPublishOutMessageHandlerTest.groovy: -------------------------------------------------------------------------------- 1 | package javasabr.mqtt.service.publish.handler.impl 2 | 3 | import javasabr.mqtt.model.MqttProperties 4 | import javasabr.mqtt.model.MqttVersion 5 | import javasabr.mqtt.model.QoS 6 | import javasabr.mqtt.model.publishing.Publish 7 | import javasabr.mqtt.model.subscriber.SingleSubscriber 8 | import javasabr.mqtt.model.subscription.Subscription 9 | import javasabr.mqtt.network.message.out.PublishMqtt5OutMessage 10 | import javasabr.mqtt.service.TestExternalNetworkMqttUser 11 | 12 | class Qos0MqttPublishOutMessageHandlerTest extends QosMqttPublishOutMessageHandlerTest { 13 | 14 | def "should deliver publish to subscriber"() { 15 | given: 16 | def publishOutHandler = new Qos0MqttPublishOutMessageHandler(defaultMessageOutFactoryService) 17 | def connection = mockedExternalConnection(MqttVersion.MQTT_5) 18 | def user = connection.user() as TestExternalNetworkMqttUser 19 | def testTopicName = defaultTopicService.createTopicName(user, "Qos0MqttPublishOutMessageHandlerTest/1") 20 | def topicFilter = defaultTopicService.createTopicFilter(user, "Qos0MqttPublishOutMessageHandlerTest/1") 21 | def subscription = Subscription.minimal(topicFilter, QoS.AT_MOST_ONCE) 22 | def subscriber = new SingleSubscriber(user, subscription) 23 | def originalMessageId = 60 24 | def testPublish = Publish.minimal(originalMessageId, QoS.EXACTLY_ONCE, testTopicName, testPayload) 25 | .withDuplicated() 26 | when: 27 | publishOutHandler.handle(testPublish, subscriber) 28 | then: 29 | with(user.nextSentMessage(PublishMqtt5OutMessage)) { 30 | qos() == QoS.AT_MOST_ONCE 31 | !duplicate() 32 | payload() == testPayload 33 | topicName() == testTopicName 34 | messageId() == MqttProperties.MESSAGE_ID_IS_NOT_SET 35 | topicAlias() == MqttProperties.TOPIC_ALIAS_NOT_SET 36 | } 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /network/src/main/java/javasabr/mqtt/network/message/out/SubscribeAckMqtt311OutMessage.java: -------------------------------------------------------------------------------- 1 | package javasabr.mqtt.network.message.out; 2 | 3 | import java.nio.ByteBuffer; 4 | import javasabr.mqtt.base.util.DebugUtils; 5 | import javasabr.mqtt.model.message.MqttMessageType; 6 | import javasabr.mqtt.model.reason.code.SubscribeAckReasonCode; 7 | import javasabr.mqtt.network.MqttConnection; 8 | import javasabr.rlib.collections.array.Array; 9 | import lombok.AccessLevel; 10 | import lombok.Getter; 11 | import lombok.experimental.Accessors; 12 | import lombok.experimental.FieldDefaults; 13 | 14 | /** 15 | * Subscribe acknowledgement. 16 | */ 17 | @Getter 18 | @Accessors 19 | @FieldDefaults(level = AccessLevel.PRIVATE, makeFinal = true) 20 | public class SubscribeAckMqtt311OutMessage extends TrackableMqttOutMessage { 21 | 22 | private static final byte MESSAGE_TYPE = (byte) MqttMessageType.SUBSCRIBE_ACK.ordinal(); 23 | 24 | static { 25 | DebugUtils.registerIncludedFields("reasonCodes", "messageId"); 26 | } 27 | 28 | /** 29 | * The order of Reason Codes in the SUBACK packet MUST match the order of Topic Filters in the SUBSCRIBE packet. 30 | */ 31 | Array reasonCodes; 32 | 33 | public SubscribeAckMqtt311OutMessage(int messageId, Array reasonCodes) { 34 | super(messageId); 35 | this.reasonCodes = reasonCodes; 36 | } 37 | 38 | @Override 39 | public int expectedLength(MqttConnection connection) { 40 | return 2 + reasonCodes.size(); 41 | } 42 | 43 | @Override 44 | protected byte messageTypeId() { 45 | return MESSAGE_TYPE; 46 | } 47 | 48 | @Override 49 | public MqttMessageType messageType() { 50 | return MqttMessageType.SUBSCRIBE_ACK; 51 | } 52 | 53 | @Override 54 | protected void writePayload(MqttConnection connection, ByteBuffer buffer) { 55 | // http://docs.oasis-open.org/mqtt/mqtt/v3.1.1/os/mqtt-v3.1.1-os.html#_Toc398718071 56 | for (var reasonCode : reasonCodes) { 57 | writeByte(buffer, reasonCode.code()); 58 | } 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /network/src/test/groovy/javasabr/mqtt/network/util/MqttDataUtilsTest.groovy: -------------------------------------------------------------------------------- 1 | package javasabr.mqtt.network.util 2 | 3 | 4 | import javasabr.mqtt.test.support.UnitSpecification 5 | 6 | import java.nio.ByteBuffer 7 | 8 | class MqttDataUtilsTest extends UnitSpecification { 9 | 10 | def "should write integer to MQTT multi byte integer successful"(int value, int expectedBytes) { 11 | given: 12 | def buffer = ByteBuffer.allocate(4) 13 | when: 14 | MqttDataUtils.writeMbi(value, buffer) 15 | then: 16 | buffer.position() == expectedBytes 17 | where: 18 | value << [10, 1000, 40_000, 500_000, 1_000_000, MqttDataUtils.MAX_MBI] 19 | expectedBytes << [1, 2, 3, 3, 3, 4] 20 | } 21 | 22 | def "should failed writing too big integer to MQTT multi byte integer"(int value) { 23 | given: 24 | def buffer = ByteBuffer.allocate(10) 25 | when: 26 | MqttDataUtils.writeMbi(value, buffer) 27 | then: 28 | thrown IllegalArgumentException 29 | where: 30 | value << [1_000_000_000, 2_000_000_000, 5_000_000_000] 31 | } 32 | 33 | def "should read integer from MQTT multi byte integer successful"(int value) { 34 | given: 35 | def buffer = ByteBuffer.allocate(5) 36 | MqttDataUtils.writeMbi(value, buffer).flip() 37 | when: 38 | def read = MqttDataUtils.readMbi(buffer) 39 | then: 40 | read == value 41 | where: 42 | value << [10, 1000, 40_000, 500_000, 1_000_000, MqttDataUtils.MAX_MBI] 43 | } 44 | 45 | def "should failed reading integer from MQTT multi byte integer"(int value, int position) { 46 | given: 47 | def buffer = ByteBuffer.allocate(10) 48 | MqttDataUtils.writeMbi(value, buffer) 49 | .position(position) 50 | .flip() 51 | when: 52 | def read = MqttDataUtils.readMbi(buffer) 53 | then: 54 | read == -1 55 | where: 56 | value << [10, 1000, 40_000, 500_000, 1_000_000, MqttDataUtils.MAX_MBI] 57 | position << [0, 0, 1, 1, 2, 2] 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /core-service/src/main/java/javasabr/mqtt/service/publish/handler/impl/Qos0MqttPublishInMessageHandler.java: -------------------------------------------------------------------------------- 1 | package javasabr.mqtt.service.publish.handler.impl; 2 | 3 | import javasabr.mqtt.model.MqttProperties; 4 | import javasabr.mqtt.model.MqttProtocolErrors; 5 | import javasabr.mqtt.model.QoS; 6 | import javasabr.mqtt.model.publishing.Publish; 7 | import javasabr.mqtt.model.reason.code.DisconnectReasonCode; 8 | import javasabr.mqtt.network.impl.ExternalNetworkMqttUser; 9 | import javasabr.mqtt.network.message.out.MqttOutMessage; 10 | import javasabr.mqtt.network.session.NetworkMqttSession; 11 | import javasabr.mqtt.service.MessageOutFactoryService; 12 | import javasabr.mqtt.service.PublishDeliveringService; 13 | import javasabr.mqtt.service.SubscriptionService; 14 | 15 | public class Qos0MqttPublishInMessageHandler extends AbstractMqttPublishInMessageHandler { 16 | 17 | public Qos0MqttPublishInMessageHandler( 18 | SubscriptionService subscriptionService, 19 | PublishDeliveringService publishDeliveringService, 20 | MessageOutFactoryService messageOutFactoryService) { 21 | super(ExternalNetworkMqttUser.class, subscriptionService, publishDeliveringService, messageOutFactoryService); 22 | } 23 | 24 | @Override 25 | public QoS qos() { 26 | return QoS.AT_MOST_ONCE; 27 | } 28 | 29 | @Override 30 | protected boolean validateImpl(ExternalNetworkMqttUser user, NetworkMqttSession session, Publish publish) { 31 | int messageId = publish.messageId(); 32 | if (messageId != MqttProperties.MESSAGE_ID_IS_NOT_SET) { 33 | handleNotExpectedMessageId(user); 34 | return false; 35 | } 36 | return super.validateImpl(user, session, publish); 37 | } 38 | 39 | private void handleNotExpectedMessageId(ExternalNetworkMqttUser user) { 40 | MqttOutMessage response = messageOutFactoryService 41 | .resolveFactory(user) 42 | .newDisconnect(user, DisconnectReasonCode.PROTOCOL_ERROR, MqttProtocolErrors.NOT_EXPECTED_MESSAGE_ID); 43 | user.closeWithReason(response); 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /acl-engine/src/testFixtures/java/javasabr/mqtt/acl/engine/ConditionMatcherAware.java: -------------------------------------------------------------------------------- 1 | package javasabr.mqtt.acl.engine; 2 | 3 | import javasabr.mqtt.acl.engine.builder.ClientMatcherBuilder; 4 | import javasabr.mqtt.acl.engine.model.condition.ClientIdCondition; 5 | import javasabr.mqtt.acl.engine.model.condition.IpAddressCondition; 6 | import javasabr.mqtt.acl.engine.model.condition.MqttUserCondition; 7 | import javasabr.mqtt.acl.engine.model.condition.TopicCondition; 8 | import javasabr.mqtt.acl.engine.model.condition.UserNameCondition; 9 | import javasabr.mqtt.acl.engine.model.matcher.EqualsMatcher; 10 | import javasabr.mqtt.acl.engine.model.matcher.TopicFilterMatcher; 11 | import javasabr.mqtt.acl.engine.model.matcher.TopicNameMatcher; 12 | import javasabr.mqtt.model.topic.TopicFilter; 13 | import javasabr.mqtt.model.topic.TopicName; 14 | import javasabr.rlib.collections.array.Array; 15 | 16 | public interface ConditionMatcherAware extends ClientMatcherBuilder { 17 | 18 | default MqttUserCondition userNameEquals(String value) { 19 | return new UserNameCondition(new EqualsMatcher(value)); 20 | } 21 | 22 | default MqttUserCondition userNameRegex(String value) { 23 | return new UserNameCondition(regex(value)); 24 | } 25 | 26 | default MqttUserCondition clientIdEquals(String value) { 27 | return new ClientIdCondition(new EqualsMatcher(value)); 28 | } 29 | 30 | default MqttUserCondition clientIdRegex(String value) { 31 | return new ClientIdCondition(regex(value)); 32 | } 33 | 34 | default MqttUserCondition ipAddressEquals(String value) { 35 | return new IpAddressCondition(new EqualsMatcher(value)); 36 | } 37 | 38 | default MqttUserCondition ipAddressRegex(String value) { 39 | return new IpAddressCondition(regex(value)); 40 | } 41 | 42 | default TopicCondition topicNameCondition(String value) { 43 | return new TopicCondition(Array.of(new TopicNameMatcher(TopicName.valueOf(value)))); 44 | } 45 | 46 | default TopicCondition topicFilterCondition(String value) { 47 | return new TopicCondition(Array.of(new TopicFilterMatcher(TopicFilter.valueOf(value)))); 48 | } 49 | } 50 | 51 | -------------------------------------------------------------------------------- /core-service/src/main/java/javasabr/mqtt/service/message/validator/PublishPayloadMqttInMessageFieldValidator.java: -------------------------------------------------------------------------------- 1 | package javasabr.mqtt.service.message.validator; 2 | 3 | import javasabr.mqtt.model.MqttProtocolErrors; 4 | import javasabr.mqtt.model.PayloadFormat; 5 | import javasabr.mqtt.model.reason.code.DisconnectReasonCode; 6 | import javasabr.mqtt.network.MqttConnection; 7 | import javasabr.mqtt.network.message.in.PublishMqttInMessage; 8 | import javasabr.mqtt.network.user.NetworkMqttUser; 9 | import javasabr.mqtt.service.MessageOutFactoryService; 10 | import lombok.AccessLevel; 11 | import lombok.CustomLog; 12 | import lombok.RequiredArgsConstructor; 13 | import lombok.experimental.FieldDefaults; 14 | 15 | @CustomLog 16 | @RequiredArgsConstructor 17 | @FieldDefaults(level = AccessLevel.PRIVATE, makeFinal = true) 18 | public class PublishPayloadMqttInMessageFieldValidator extends 19 | MqttInMessageFieldValidator { 20 | 21 | public static final int ORDER = 10; 22 | 23 | MessageOutFactoryService messageOutFactoryService; 24 | 25 | @Override 26 | public boolean isNotValid(MqttConnection connection, NetworkMqttUser user, PublishMqttInMessage message) { 27 | byte[] payload = message.payload(); 28 | if (payload == null) { 29 | log.warning(user.clientId(), "[%s] Missed payload"::formatted); 30 | return true; 31 | } 32 | PayloadFormat payloadFormat = message.payloadFormat(); 33 | if (payloadFormat == PayloadFormat.INVALID) { 34 | log.warning(user.clientId(), "[%s] Provided invalid PayloadFormat"::formatted); 35 | handleInvalidPayloadFormat(user); 36 | return true; 37 | } 38 | return false; 39 | } 40 | 41 | @Override 42 | public int order() { 43 | return ORDER; 44 | } 45 | 46 | private void handleInvalidPayloadFormat(NetworkMqttUser user) { 47 | user.closeWithReason(messageOutFactoryService 48 | .resolveFactory(user) 49 | .newDisconnect( 50 | user, 51 | DisconnectReasonCode.PROTOCOL_ERROR, 52 | MqttProtocolErrors.PROVIDED_INVALID_PAYLOAD_FORMAT)); 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /core-service/src/main/java/javasabr/mqtt/service/message/validator/PublishResponseTopicMqttInMessageFieldValidator.java: -------------------------------------------------------------------------------- 1 | package javasabr.mqtt.service.message.validator; 2 | 3 | import javasabr.mqtt.model.MqttProtocolErrors; 4 | import javasabr.mqtt.model.reason.code.DisconnectReasonCode; 5 | import javasabr.mqtt.model.topic.TopicValidator; 6 | import javasabr.mqtt.network.MqttConnection; 7 | import javasabr.mqtt.network.message.in.PublishMqttInMessage; 8 | import javasabr.mqtt.network.user.NetworkMqttUser; 9 | import javasabr.mqtt.service.MessageOutFactoryService; 10 | import lombok.AccessLevel; 11 | import lombok.CustomLog; 12 | import lombok.RequiredArgsConstructor; 13 | import lombok.experimental.FieldDefaults; 14 | 15 | @CustomLog 16 | @RequiredArgsConstructor 17 | @FieldDefaults(level = AccessLevel.PRIVATE, makeFinal = true) 18 | public class PublishResponseTopicMqttInMessageFieldValidator extends 19 | MqttInMessageFieldValidator { 20 | 21 | public static final int ORDER = PublishMessageExpiryIntervalMqttInMessageFieldValidator.ORDER + 1; 22 | 23 | MessageOutFactoryService messageOutFactoryService; 24 | 25 | @Override 26 | public boolean isNotValid(MqttConnection connection, NetworkMqttUser user, PublishMqttInMessage message) { 27 | String rawResponseTopicName = message.rawResponseTopicName(); 28 | if (rawResponseTopicName != null) { 29 | if (!TopicValidator.validateTopicName(rawResponseTopicName)) { 30 | log.warning(user.clientId(), rawResponseTopicName, "[%s] Provided invalid ResponseTopic:[%s]"::formatted); 31 | handleInvalidResponseTopicName(user); 32 | return true; 33 | } 34 | } 35 | return false; 36 | } 37 | 38 | @Override 39 | public int order() { 40 | return ORDER; 41 | } 42 | 43 | private void handleInvalidResponseTopicName(NetworkMqttUser user) { 44 | user.closeWithReason(messageOutFactoryService 45 | .resolveFactory(user) 46 | .newDisconnect( 47 | user, 48 | DisconnectReasonCode.PROTOCOL_ERROR, 49 | MqttProtocolErrors.PROVIDED_INVALID_RESPONSE_TOPIC)); 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /core-service/src/main/java/javasabr/mqtt/service/message/validator/PublishMessageExpiryIntervalMqttInMessageFieldValidator.java: -------------------------------------------------------------------------------- 1 | package javasabr.mqtt.service.message.validator; 2 | 3 | import javasabr.mqtt.model.MqttProperties; 4 | import javasabr.mqtt.model.MqttProtocolErrors; 5 | import javasabr.mqtt.model.reason.code.DisconnectReasonCode; 6 | import javasabr.mqtt.network.MqttConnection; 7 | import javasabr.mqtt.network.message.in.PublishMqttInMessage; 8 | import javasabr.mqtt.network.user.NetworkMqttUser; 9 | import javasabr.mqtt.service.MessageOutFactoryService; 10 | import lombok.AccessLevel; 11 | import lombok.CustomLog; 12 | import lombok.RequiredArgsConstructor; 13 | import lombok.experimental.FieldDefaults; 14 | 15 | @CustomLog 16 | @RequiredArgsConstructor 17 | @FieldDefaults(level = AccessLevel.PRIVATE, makeFinal = true) 18 | public class PublishMessageExpiryIntervalMqttInMessageFieldValidator extends 19 | MqttInMessageFieldValidator { 20 | 21 | public static final int ORDER = PublishRetainMqttInMessageFieldValidator.ORDER + 1; 22 | 23 | MessageOutFactoryService messageOutFactoryService; 24 | 25 | @Override 26 | public boolean isNotValid(MqttConnection connection, NetworkMqttUser user, PublishMqttInMessage message) { 27 | long messageExpiryInterval = message.messageExpiryInterval(); 28 | if (messageExpiryInterval != MqttProperties.MESSAGE_EXPIRY_INTERVAL_IS_NOT_SET 29 | && messageExpiryInterval < MqttProperties.MESSAGE_EXPIRY_INTERVAL_MIN) { 30 | log.warning(user.clientId(), "[%s] Provided invalid MessageExpiryInterval"::formatted); 31 | handleInvalidMessageExpiryInterval(user); 32 | return true; 33 | } 34 | return false; 35 | } 36 | 37 | @Override 38 | public int order() { 39 | return ORDER; 40 | } 41 | 42 | private void handleInvalidMessageExpiryInterval(NetworkMqttUser user) { 43 | user.closeWithReason(messageOutFactoryService 44 | .resolveFactory(user) 45 | .newDisconnect( 46 | user, 47 | DisconnectReasonCode.PROTOCOL_ERROR, 48 | MqttProtocolErrors.PROVIDED_INVALID_MESSAGE_EXPIRY_INTERVAL)); 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /core-service/src/main/java/javasabr/mqtt/service/publish/handler/PublishHandlingResult.java: -------------------------------------------------------------------------------- 1 | package javasabr.mqtt.service.publish.handler; 2 | 3 | import javasabr.mqtt.model.reason.code.PublishAckReasonCode; 4 | import javasabr.mqtt.model.reason.code.PublishReceivedReasonCode; 5 | import lombok.AccessLevel; 6 | import lombok.Getter; 7 | import lombok.RequiredArgsConstructor; 8 | import lombok.experimental.Accessors; 9 | import lombok.experimental.FieldDefaults; 10 | 11 | @Getter 12 | @RequiredArgsConstructor 13 | @Accessors(fluent = true, chain = false) 14 | @FieldDefaults(level = AccessLevel.PRIVATE, makeFinal = true) 15 | public enum PublishHandlingResult { 16 | SUCCESS(false, PublishAckReasonCode.SUCCESS, PublishReceivedReasonCode.SUCCESS), 17 | SKIPPED(false, PublishAckReasonCode.SUCCESS, PublishReceivedReasonCode.SUCCESS), 18 | 19 | // ERRORS 20 | UNSPECIFIED_ERROR(true, PublishAckReasonCode.UNSPECIFIED_ERROR, PublishReceivedReasonCode.UNSPECIFIED_ERROR), 21 | IMPLEMENTATION_SPECIFIC_ERROR( 22 | true, 23 | PublishAckReasonCode.IMPLEMENTATION_SPECIFIC_ERROR, 24 | PublishReceivedReasonCode.IMPLEMENTATION_SPECIFIC_ERROR), 25 | NOT_AUTHORIZED(true, PublishAckReasonCode.NOT_AUTHORIZED, PublishReceivedReasonCode.NOT_AUTHORIZED), 26 | TOPIC_NAME_INVALID(true, PublishAckReasonCode.TOPIC_NAME_INVALID, PublishReceivedReasonCode.TOPIC_NAME_INVALID), 27 | PACKET_IDENTIFIER_IN_USE( 28 | true, 29 | PublishAckReasonCode.PACKET_IDENTIFIER_IN_USE, 30 | PublishReceivedReasonCode.PACKET_IDENTIFIER_IN_USE), 31 | QUOTA_EXCEEDED(true, PublishAckReasonCode.QUOTA_EXCEEDED, PublishReceivedReasonCode.QUOTA_EXCEEDED), 32 | PAYLOAD_FORMAT_INVALID( 33 | true, 34 | PublishAckReasonCode.PAYLOAD_FORMAT_INVALID, 35 | PublishReceivedReasonCode.PAYLOAD_FORMAT_INVALID), 36 | 37 | // CUSTOM 38 | NOT_EXPECTED_CLIENT( 39 | true, 40 | PublishAckReasonCode.UNSPECIFIED_ERROR, 41 | PublishReceivedReasonCode.UNSPECIFIED_ERROR), 42 | SESSION_IS_ALREADY_CLOSED( 43 | true, 44 | PublishAckReasonCode.UNSPECIFIED_ERROR, 45 | PublishReceivedReasonCode.UNSPECIFIED_ERROR); 46 | 47 | boolean error; 48 | PublishAckReasonCode ackReasonCode; 49 | PublishReceivedReasonCode receivedReasonCode; 50 | } 51 | -------------------------------------------------------------------------------- /model/src/main/java/javasabr/mqtt/model/subscription/Subscription.java: -------------------------------------------------------------------------------- 1 | package javasabr.mqtt.model.subscription; 2 | 3 | import com.fasterxml.jackson.annotation.JsonValue; 4 | import javasabr.mqtt.model.MqttProperties; 5 | import javasabr.mqtt.model.QoS; 6 | import javasabr.mqtt.model.SubscribeRetainHandling; 7 | import javasabr.mqtt.model.topic.TopicFilter; 8 | 9 | public record Subscription( 10 | /* 11 | The subscriber's topic filter. 12 | */ 13 | TopicFilter topicFilter, 14 | /* 15 | * The associated ID for the subscription 16 | */ 17 | int subscriptionId, 18 | /* 19 | Maximum QoS field. This gives the maximum QoS level at which the Server can send Application Messages to the 20 | Client. 21 | */ 22 | QoS qos, 23 | /* 24 | This option specifies whether retained messages are sent when the subscription is established. This does not affect 25 | the sending of retained messages at any point after the subscribe. If there are no retained messages matching the 26 | Topic Filter, all of these values act the same. 27 | */ 28 | SubscribeRetainHandling retainHandling, 29 | /* 30 | If the value is true, Application Messages MUST NOT be forwarded to a connection with a ClientID equal to the 31 | ClientID of the publishing connection. 32 | */ 33 | boolean noLocal, 34 | /* 35 | If true, Application Messages forwarded using this subscription keep the RETAIN flag they were published with. If 36 | false, Application Messages forwarded using this subscription have the RETAIN flag set to 0. Retained messages sent 37 | when the subscription is established have the RETAIN flag set to 1. 38 | */ 39 | boolean retainAsPublished) { 40 | 41 | public static Subscription minimal(TopicFilter topicFilter, QoS qos) { 42 | return new Subscription( 43 | topicFilter, 44 | MqttProperties.SUBSCRIPTION_ID_IS_NOT_SET, 45 | qos, 46 | SubscribeRetainHandling.SEND, 47 | true, 48 | true); 49 | } 50 | 51 | @JsonValue 52 | @Override 53 | public String toString() { 54 | return "[" + topicFilter.rawTopic() + "|" + qos.level() + "|" + retainHandling + "|" + noLocal + "|" + retainAsPublished + "]"; 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /acl-engine/src/test/groovy/javasabr/mqtt/acl/engine/model/rule/RuleTest.groovy: -------------------------------------------------------------------------------- 1 | package javasabr.mqtt.acl.engine.model.rule 2 | 3 | import javasabr.mqtt.acl.engine.ConditionMatcherAware 4 | import javasabr.mqtt.acl.engine.model.condition.TopicCondition 5 | import javasabr.mqtt.model.acl.Operation 6 | import javasabr.mqtt.model.subscription.TestMqttUser 7 | import javasabr.mqtt.model.topic.TopicName 8 | import javasabr.mqtt.test.support.UnitSpecification 9 | 10 | import static javasabr.mqtt.model.acl.Operation.PUBLISH 11 | import static javasabr.mqtt.model.acl.Operation.SUBSCRIBE 12 | 13 | class RuleTest extends UnitSpecification implements ConditionMatcherAware { 14 | 15 | def "should test rule"(Rule rule, Operation operation, boolean expectedResult) { 16 | given: 17 | def user = new TestMqttUser("clientId") 18 | def topic = TopicName.valueOf("topic") 19 | when: 20 | boolean result = rule.test(user, operation, topic) 21 | then: 22 | result == expectedResult 23 | where: 24 | operation | rule | expectedResult 25 | PUBLISH | new AllowPublishRule(clientIdEquals("clientId"), TopicCondition.MATCH_ANY) | true 26 | SUBSCRIBE | new AllowPublishRule(clientIdEquals("clientId"), TopicCondition.MATCH_ANY) | false 27 | PUBLISH | new AllowSubscribeRule(clientIdEquals("clientId"), TopicCondition.MATCH_ANY) | false 28 | SUBSCRIBE | new AllowSubscribeRule(clientIdEquals("clientId"), TopicCondition.MATCH_ANY) | true 29 | PUBLISH | new DenyPublishRule(clientIdEquals("clientId"), TopicCondition.MATCH_ANY) | true 30 | SUBSCRIBE | new DenyPublishRule(clientIdEquals("clientId"), TopicCondition.MATCH_ANY) | false 31 | PUBLISH | new DenySubscribeRule(clientIdEquals("clientId"), TopicCondition.MATCH_ANY) | false 32 | SUBSCRIBE | new DenySubscribeRule(clientIdEquals("clientId"), TopicCondition.MATCH_ANY) | true 33 | PUBLISH | new AllowPublishRule(clientIdEquals("clientId"), topicNameCondition("other/topic")) | false 34 | PUBLISH | new AllowPublishRule(clientIdEquals("otherClientId"), TopicCondition.MATCH_ANY) | false 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /application/src/test/groovy/javasabr/mqtt/broker/application/service/DisabledFeaturesSubscribtionServiceTest.groovy: -------------------------------------------------------------------------------- 1 | package javasabr.mqtt.broker.application.service 2 | 3 | import com.hivemq.client.mqtt.MqttClientState 4 | import com.hivemq.client.mqtt.mqtt5.exceptions.Mqtt5SubAckException 5 | import com.hivemq.client.mqtt.mqtt5.message.subscribe.suback.Mqtt5SubAckReasonCode 6 | import javasabr.mqtt.broker.application.IntegrationSpecification 7 | import org.springframework.test.context.TestPropertySource 8 | import spock.lang.Unroll 9 | 10 | import java.util.concurrent.CompletionException 11 | 12 | import static com.hivemq.client.mqtt.mqtt5.message.subscribe.suback.Mqtt5SubAckReasonCode.SHARED_SUBSCRIPTIONS_NOT_SUPPORTED 13 | import static com.hivemq.client.mqtt.mqtt5.message.subscribe.suback.Mqtt5SubAckReasonCode.WILDCARD_SUBSCRIPTIONS_NOT_SUPPORTED 14 | 15 | @TestPropertySource(locations = "classpath:disabled-features.properties") 16 | class DisabledFeaturesSubscribtionServiceTest extends IntegrationSpecification { 17 | 18 | @Unroll 19 | def "should reject subscribe with wrong topic filter"( 20 | String wrongTopicFilter, 21 | Mqtt5SubAckReasonCode reasonCode 22 | ) { 23 | given: 24 | def subscriber = buildExternalMqtt5Client() 25 | when: 26 | subscriber.connectWith() 27 | .send() 28 | .join() 29 | subscriber.subscribeWith() 30 | .topicFilter(wrongTopicFilter) 31 | .send() 32 | .join() 33 | then: 34 | Thread.sleep(50) 35 | subscriber.state == MqttClientState.DISCONNECTED 36 | def ex = thrown CompletionException 37 | if (ex.cause != null) { 38 | ex.cause.class == Mqtt5SubAckException 39 | ex.cause.message == "SUBACK contains only Error Codes" 40 | ((Mqtt5SubAckException) ex.cause).mqttMessage.reasonCodes.contains(reasonCode) 41 | } 42 | where: 43 | wrongTopicFilter | reasonCode 44 | "topic/+" | WILDCARD_SUBSCRIPTIONS_NOT_SUPPORTED 45 | "topic/#" | WILDCARD_SUBSCRIPTIONS_NOT_SUPPORTED 46 | "topic/+/Filter" | WILDCARD_SUBSCRIPTIONS_NOT_SUPPORTED 47 | "\$share/group/topic/Filter" | SHARED_SUBSCRIPTIONS_NOT_SUPPORTED 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /model/src/main/java/javasabr/mqtt/model/topic/AbstractTopic.java: -------------------------------------------------------------------------------- 1 | package javasabr.mqtt.model.topic; 2 | 3 | import javasabr.mqtt.base.util.DebugUtils; 4 | import javasabr.rlib.common.util.StringUtils; 5 | import lombok.AccessLevel; 6 | import lombok.EqualsAndHashCode; 7 | import lombok.Getter; 8 | import lombok.experimental.Accessors; 9 | import lombok.experimental.FieldDefaults; 10 | 11 | @Getter 12 | @EqualsAndHashCode(of = "rawTopic") 13 | @Accessors(fluent = true, chain = false) 14 | @FieldDefaults(level = AccessLevel.PROTECTED, makeFinal = true) 15 | public abstract class AbstractTopic { 16 | 17 | public static final String DELIMITER = "/"; 18 | public static final char DELIMITER_CHAR = '/'; 19 | 20 | 21 | static { 22 | DebugUtils.registerIncludedFields("rawTopic"); 23 | } 24 | 25 | String[] segments; 26 | String rawTopic; 27 | int length; 28 | 29 | protected AbstractTopic(String rawTopicName) { 30 | length = rawTopicName.length(); 31 | segments = splitTopic(rawTopicName); 32 | rawTopic = rawTopicName; 33 | } 34 | 35 | public String segment(int level) { 36 | return segments[level]; 37 | } 38 | 39 | public int levelsCount() { 40 | return segments.length; 41 | } 42 | 43 | String lastSegment() { 44 | return segments[segments.length - 1]; 45 | } 46 | 47 | public boolean isInvalid() { 48 | return false; 49 | } 50 | 51 | @Override 52 | public String toString() { 53 | return rawTopic; 54 | } 55 | 56 | protected static String[] splitTopic(String topic) { 57 | int segmentCount = countOccurrencesOf(topic, AbstractTopic.DELIMITER) + 1; 58 | var segments = new String[segmentCount]; 59 | int i = 0, pos = 0, end; 60 | while ((end = topic.indexOf(AbstractTopic.DELIMITER, pos)) >= 0) { 61 | segments[i++] = topic.substring(pos, end); 62 | pos = end + 1; 63 | } 64 | segments[i] = topic.substring(pos); 65 | return segments; 66 | } 67 | 68 | protected static int countOccurrencesOf(String str, String sub) { 69 | if (StringUtils.isEmpty(str)) { 70 | return 0; 71 | } 72 | int count = 0; 73 | int pos = 0; 74 | int idx; 75 | while ((idx = str.indexOf(sub, pos)) != -1) { 76 | ++count; 77 | pos = idx + sub.length(); 78 | } 79 | return count; 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /network/src/test/groovy/javasabr/mqtt/network/message/out/ConnectMqtt5OutMessageTest.groovy: -------------------------------------------------------------------------------- 1 | package javasabr.mqtt.network.message.out 2 | 3 | import javasabr.mqtt.model.QoS 4 | import javasabr.mqtt.network.message.in.ConnectMqttInMessage 5 | import javasabr.rlib.common.util.ArrayUtils 6 | import javasabr.rlib.common.util.BufferUtils 7 | 8 | class ConnectMqtt5OutMessageTest extends BaseMqttOutMessageTest { 9 | 10 | def "should write message correctly"() { 11 | given: 12 | def outMessage = new ConnectMqtt5OutMessage( 13 | userName, 14 | "", 15 | mqtt311ClientId, 16 | userPassword, 17 | ArrayUtils.EMPTY_BYTE_ARRAY, 18 | QoS.AT_MOST_ONCE, 19 | keepAlive, 20 | willRetain, 21 | cleanStart, 22 | testUserProperties, 23 | authMethod, 24 | authData, 25 | sessionExpiryInterval, 26 | receiveMaxPublishes, 27 | maxMessageSize, 28 | topicAliasMaxValue, 29 | requestResponseInformation, 30 | requestProblemInformation) 31 | when: 32 | def dataBuffer = BufferUtils.prepareBuffer(512) { 33 | outMessage.write(defaultMqtt5Connection, it) 34 | } 35 | def reader = new ConnectMqttInMessage(0b0001_0000 as byte) 36 | def result = reader.read(defaultMqtt5Connection, dataBuffer, dataBuffer.limit()) 37 | then: 38 | result 39 | reader.username() == userName 40 | reader.clientId() == mqtt311ClientId 41 | reader.password() == userPassword 42 | reader.keepAlive() == keepAlive 43 | reader.userProperties() == testUserProperties 44 | reader.cleanStart() == cleanStart 45 | reader.willRetain() == willRetain 46 | reader.authenticationMethod() == authMethod 47 | reader.authenticationData() == authData 48 | reader.sessionExpiryInterval() == sessionExpiryInterval 49 | reader.receiveMaxPublishes() == receiveMaxPublishes 50 | reader.maxPacketSize() == maxMessageSize 51 | reader.topicAliasMaxValue() == topicAliasMaxValue 52 | reader.requestResponseInformation() == requestResponseInformation 53 | reader.requestProblemInformation() == requestProblemInformation 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /model/src/main/java/javasabr/mqtt/model/reason/code/PublishAckReasonCode.java: -------------------------------------------------------------------------------- 1 | package javasabr.mqtt.model.reason.code; 2 | 3 | import javasabr.rlib.common.util.NumberedEnum; 4 | import javasabr.rlib.common.util.NumberedEnumMap; 5 | import lombok.Getter; 6 | import lombok.RequiredArgsConstructor; 7 | import lombok.experimental.Accessors; 8 | 9 | @Getter 10 | @Accessors 11 | @RequiredArgsConstructor 12 | public enum PublishAckReasonCode implements NumberedEnum, ReasonCode { 13 | /** 14 | * The message is accepted. Publication of the QoS 1 message proceeds. 15 | */ 16 | SUCCESS(0x00), 17 | /** 18 | * The message is accepted but there are no subscribers. This is sent only by the Server. If the Server knows that 19 | * there are no matching subscribers, it MAY use this Reason Code instead of 0x00 (Success). 20 | */ 21 | NO_MATCHING_SUBSCRIBERS(0x10), 22 | 23 | // ERRORS 24 | 25 | /** 26 | * The receiver does not accept the publish but either does not want to reveal the reason, or it does not match one of 27 | * the other values. 28 | */ 29 | UNSPECIFIED_ERROR(0x80), 30 | /** 31 | * The PUBLISH is valid but the receiver is not willing to accept it. 32 | */ 33 | IMPLEMENTATION_SPECIFIC_ERROR(0x83), 34 | /** 35 | * The PUBLISH is not authorized. 36 | */ 37 | NOT_AUTHORIZED(0x87), 38 | /** 39 | * The Topic Name is not malformed, but is not accepted by this Client or Server. 40 | */ 41 | TOPIC_NAME_INVALID(0x90), 42 | /** 43 | * The Packet Identifier is already in use. This might indicate a mismatch in the Session State between the Client and 44 | * Server. 45 | */ 46 | PACKET_IDENTIFIER_IN_USE(0x91), 47 | /** 48 | * An implementation or administrative imposed limit has been exceeded. 49 | */ 50 | QUOTA_EXCEEDED(0x97), 51 | /** 52 | * The payload format does not match the specified Payload Format Indicator. 53 | */ 54 | PAYLOAD_FORMAT_INVALID(0x99); 55 | 56 | private static final NumberedEnumMap NUMBERED_MAP = 57 | new NumberedEnumMap<>(PublishAckReasonCode.class); 58 | 59 | public static PublishAckReasonCode ofCode(int code) { 60 | return NUMBERED_MAP.require(code); 61 | } 62 | 63 | private final int code; 64 | 65 | @Override 66 | public int number() { 67 | return code; 68 | } 69 | } 70 | --------------------------------------------------------------------------------