Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -5,4 +5,8 @@ public class AclConfigurationException extends RuntimeException {
public AclConfigurationException(String message) {
super(message);
}

public AclConfigurationException(String message, Throwable cause) {
super(message, cause);
}
}
Original file line number Diff line number Diff line change
@@ -1,26 +1,14 @@
package javasabr.mqtt.acl.groovy.dsl.loader

import javasabr.mqtt.acl.engine.builder.RuleContainerBuilder
import javasabr.mqtt.acl.engine.exception.AclConfigurationException
import javasabr.mqtt.acl.engine.model.rule.AclRule
import javasabr.mqtt.acl.groovy.dsl.builder.AclRulesBuilder
import javasabr.mqtt.model.acl.Operation
import javasabr.rlib.collections.array.Array

import java.nio.file.Files
import java.nio.file.Path

class AclRulesLoader {

static Map<Operation, Array<AclRule>> load(String aclConfigPath) {
return load(Path.of(aclConfigPath))
}

static Map<Operation, Array<AclRule>> load(Path aclConfigPath) {
if (Files.notExists(aclConfigPath)) {
throw new AclConfigurationException("Config file:[%s] doesn't exist".formatted(aclConfigPath))
}

static Map<Operation, Array<AclRule>> load(InputStream aclConfigPath) {
AclRulesBuilder aclRulesBuilder = new AclRulesBuilder()

def binding = new Binding()
Expand All @@ -32,7 +20,7 @@ class AclRulesLoader {
}

def groovyShell = new GroovyShell(binding)
groovyShell.evaluate(aclConfigPath.toFile())
groovyShell.evaluate(new InputStreamReader(aclConfigPath))

return RuleContainerBuilder.groupRulesByOperation(aclRulesBuilder.build())
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,9 @@ import javasabr.mqtt.service.acl.TestRulesGenerator
import javasabr.mqtt.test.support.UnitSpecification
import javasabr.rlib.collections.array.Array

import java.nio.file.Files
import java.nio.file.NoSuchFileException
import java.nio.file.Path
import java.util.concurrent.CompletionException

import static javasabr.mqtt.acl.engine.model.Action.ALLOW
Expand All @@ -38,29 +41,29 @@ class AclRulesLoaderTest extends UnitSpecification {
given:
def ruleFile = TestRulesGenerator.generate(100)
when:
def load = AclRulesLoader.load(ruleFile.toString())
def load = AclRulesLoader.load(new FileInputStream(ruleFile))
then:
load.get(SUBSCRIBE).size() == 50
load.get(PUBLISH).size() == 50
ruleFile.delete()
}

def "should throw exception if config not exists"(String configPath, String errorMessage) {
def "should throw exception if config not exists"(String configPath) {
when:
AclRulesLoader.load(configPath)
AclRulesLoader.load(Files.newInputStream(Path.of(configPath)))
then:
def exception = thrown(AclConfigurationException)
exception.message == errorMessage
def exception = thrown(NoSuchFileException)
exception.message == configPath
where:
configPath | errorMessage
"not/existed/path" | 'Config file:[not/existed/path] doesn\'t exist'
configPath | _
"not/existed/path" | _
}

def "should work fine with only publish rules"() {
given:
def onlyPublishRulesAclPath = getAbsolutePath("acl/config/acl-publish-only.gacl")
when:
def ruleMap = AclRulesLoader.load(onlyPublishRulesAclPath)
def ruleMap = AclRulesLoader.load(Files.newInputStream(Path.of(onlyPublishRulesAclPath)))
then:
noExceptionThrown()
!ruleMap.get(PUBLISH).isEmpty()
Expand All @@ -71,7 +74,7 @@ class AclRulesLoaderTest extends UnitSpecification {
given:
def invalidAclPath = getAbsolutePath("acl/config/invalid/${invalidAclFileName}")
when:
AclRulesLoader.load(invalidAclPath)
AclRulesLoader.load(Files.newInputStream(Path.of(invalidAclPath)))
then:
def exception = thrown CompletionException
exceptionClass.isInstance exception.cause
Expand All @@ -98,7 +101,7 @@ class AclRulesLoaderTest extends UnitSpecification {
def "should parse Groovy DSL config"() {
when:
def absolutePath = getAbsolutePath("acl/config/acl.gacl")
def rules = AclRulesLoader.load(absolutePath)
def rules = AclRulesLoader.load(Files.newInputStream(Path.of(absolutePath)))
then:
verifyAll(rules.get(PUBLISH)) {
size() == 3
Expand Down
Original file line number Diff line number Diff line change
@@ -1,14 +1,15 @@
package javasabr.mqtt.acl.service.impl;

import java.io.IOException;
import java.io.InputStream;
import java.net.URI;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.Map;
import javasabr.mqtt.acl.engine.AclEngine;
import javasabr.mqtt.acl.engine.exception.AclConfigurationException;
import javasabr.mqtt.acl.engine.model.rule.AclRule;
import javasabr.mqtt.acl.groovy.dsl.loader.AclRulesLoader;
import javasabr.mqtt.acl.service.AclEngineBasedAuthorizationService;
import javasabr.mqtt.base.util.ClassPathResourceResolver;
import javasabr.mqtt.model.acl.Operation;
import javasabr.rlib.collections.array.Array;
import lombok.CustomLog;
Expand All @@ -17,15 +18,13 @@
public class GroovyDslBasedAuthorizationService extends AclEngineBasedAuthorizationService {

public void loadFrom(URI resource) {
Path localFile = Path.of(resource);
if (Files.notExists(localFile)) {
throw new AclConfigurationException("ACL configuration:[%s] doesn't exist".formatted(resource));
} else if (Files.isDirectory(localFile)) {
throw new AclConfigurationException("ACL configuration:[%s] is directory".formatted(resource));
try (InputStream aclInputStream = ClassPathResourceResolver.newInputStream(resource)) {
Map<Operation, Array<AclRule>> loadedAclRulesMap = AclRulesLoader.load(aclInputStream);
switchTo(new AclEngine(loadedAclRulesMap));
log.info(resource, loadedAclRulesMap, GroovyDslBasedAuthorizationService::buildServiceDescription);
} catch (IOException e) {
throw new AclConfigurationException("ACL configuration issue:[%s]".formatted(resource), e);
}
Map<Operation, Array<AclRule>> loadedAclRulesMap = AclRulesLoader.load(localFile);
switchTo(new AclEngine(loadedAclRulesMap));
log.info(resource, loadedAclRulesMap, GroovyDslBasedAuthorizationService::buildServiceDescription);
}

private static String buildServiceDescription(URI resource, Map<Operation, Array<AclRule>> aclRulesMap) {
Expand Down
63 changes: 60 additions & 3 deletions application/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ plugins {
id("configure-java")
id("groovy")
id("org.springframework.boot")
id 'org.graalvm.buildtools.native'
}

description = "Standard configuration of standalone version of MQTT Broker"
Expand All @@ -16,10 +17,12 @@ dependencies {
implementation libs.springboot.starter.core
implementation libs.springboot.starter.log4j2

runtimeOnly projects.credentialsSourceFile
runtimeOnly projects.authenticationProviderBasic

testImplementation projects.testSupport
testImplementation testFixtures(projects.network)
testImplementation projects.credentialsSourceFile
testImplementation projects.authenticationProviderBasic
testImplementation libs.junit.platform.testkit
}

tasks.withType(GroovyCompile).configureEach {
Expand All @@ -28,10 +31,64 @@ tasks.withType(GroovyCompile).configureEach {
}

bootRun {
mainClass = "javasabr.mqtt.broker.application.MqttBrokerApplication"
jvmArgs += "--enable-preview"
}

bootJar {
mainClass = "javasabr.mqtt.broker.application.MqttBrokerApplication"
}

graalvmNative {
toolchainDetection = true
binaries {
main {
sharedLibrary = false
buildArgs.add("--enable-preview")
javaLauncher = javaToolchains.launcherFor {
languageVersion = JavaLanguageVersion.of(25)
vendor = JvmVendorSpec.matching("GraalVM Community")
}
}
}
}

jar {
from(sourceSets.aot.output)
}

processAot {
jvmArgs([
'-Dauthentication.credentials-source.database.enabled=true',
'-Dauthentication.credentials-source.file.enabled=true',
'-Dauthentication.provider.basic.enabled=true',
'-Dmqtt.external.tls.network.enabled=true',
])
}

tasks.register('copyNativeImageTestResources', Copy) {
mustRunAfter 'nativeCompile'
from('src/test/resources/application-test.properties') {
rename 'application-test.properties', "application.properties"
}
from('src/test/resources/log4j2-test.xml') {
rename 'log4j2-test.xml', "log4j2.xml"
}
from('src/test/resources/auth/credentials-test') {
into 'auth'
}
from('src/test/resources/test-acl.gacl')
into layout.buildDirectory.dir("native/nativeCompile")
}

tasks.register('nativeImageTest', Test) {
dependsOn 'nativeCompile', 'copyNativeImageTestResources'
useJUnitPlatform()

group = 'verification'
testClassesDirs = sourceSets.test.output.classesDirs
classpath = sourceSets.test.runtimeClasspath

filter {
includeTestsMatching 'javasabr.mqtt.broker.application.NativeImageVerificationTest'
}
}
Original file line number Diff line number Diff line change
@@ -1,13 +1,17 @@
package javasabr.mqtt.broker.application;

import javasabr.mqtt.broker.application.config.MqttBrokerSpringConfig;
import javasabr.mqtt.broker.application.config.NativeConfigurationHints;
import lombok.RequiredArgsConstructor;
import org.springframework.aot.hint.annotation.RegisterReflectionForBinding;
import org.springframework.boot.SpringApplication;
import org.springframework.context.annotation.Import;
import org.springframework.context.annotation.ImportRuntimeHints;

@Import({
MqttBrokerSpringConfig.class
})
@ImportRuntimeHints(NativeConfigurationHints.class)
@RequiredArgsConstructor
public class MqttBrokerApplication {
static void main(String[] args) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@
import javasabr.mqtt.service.session.MqttSessionService;
import javasabr.mqtt.service.session.impl.InMemoryMqttSessionService;
import lombok.CustomLog;
import org.springframework.aot.hint.annotation.RegisterReflectionForBinding;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.context.annotation.Bean;
Expand Down Expand Up @@ -129,6 +130,7 @@ PublishDataStorage publishDataStorage() {
}

@Bean
@RegisterReflectionForBinding(java.util.UUID[].class)
IncomingPublishStorage incomingPublishStorage(
PublishDataStorage publishDataStorage,
@Value("${in.memory.incoming.publish.storage.clean.interval.ms:60000}") int cleanInterval) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
import javasabr.rlib.network.ServerNetworkConfig;
import javasabr.rlib.network.server.ServerNetwork;
import lombok.CustomLog;
import org.springframework.aot.hint.annotation.RegisterReflectionForBinding;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.boot.context.event.ApplicationStartedEvent;
Expand Down Expand Up @@ -62,6 +63,11 @@ InetSocketAddress externalPlainNetworkAddress(
}

@Bean
@RegisterReflectionForBinding({
java.util.function.Consumer[].class,
java.util.function.BiConsumer[].class,
reactor.core.publisher.FluxSink[].class
})
ServerNetwork<MqttConnection> externalPlainNetwork(
ServerNetworkConfig externalPlainNetworkConfig,
MqttConnectionFactory externalPlainConnectionFactory) {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
package javasabr.mqtt.broker.application.config;

import org.springframework.aot.hint.MemberCategory;
import org.springframework.aot.hint.RuntimeHints;
import org.springframework.aot.hint.RuntimeHintsRegistrar;
import org.springframework.aot.hint.TypeReference;
import org.springframework.beans.factory.config.BeanDefinition;
import org.springframework.context.annotation.ClassPathScanningCandidateComponentProvider;
import org.springframework.core.type.filter.AssignableTypeFilter;

public class NativeConfigurationHints implements RuntimeHintsRegistrar {

private static final String[] REFLECTION_PACKAGES = {
"javasabr.mqtt",
};

private static final String[] ARRAY_PACKAGES = {
"javasabr.mqtt",
"javasabr.rlib",
};

private static final String[] JDK_ARRAY_TYPES = {
// "java.util.function.Consumer[]",
// "java.util.function.BiConsumer[]",
// "java.nio.ByteBuffer[]",
// "reactor.core.publisher.FluxSink[]",
// "java.lang.String[]",
// "java.util.UUID[]",
};

@Override
public void registerHints(RuntimeHints hints, ClassLoader classLoader) {
var scanner = new ClassPathScanningCandidateComponentProvider(false);
scanner.addIncludeFilter(new AssignableTypeFilter(Object.class));

for (String pkg : REFLECTION_PACKAGES) {
for (BeanDefinition candidate : scanner.findCandidateComponents(pkg)) {
hints
.reflection()
.registerType(
TypeReference.of(candidate.getBeanClassName()),
MemberCategory.INVOKE_PUBLIC_CONSTRUCTORS,
MemberCategory.INVOKE_PUBLIC_METHODS,
MemberCategory.INVOKE_DECLARED_METHODS,
MemberCategory.ACCESS_DECLARED_FIELDS);
}
}

var arrayScanner = new ClassPathScanningCandidateComponentProvider(false);
arrayScanner.addIncludeFilter(new AssignableTypeFilter(Object.class));

for (String pkg : ARRAY_PACKAGES) {
for (var candidate : arrayScanner.findCandidateComponents(pkg)) {
hints
.reflection()
.registerType(TypeReference.of(candidate.getBeanClassName() + "[]"));
}
}

for (String arrayType : JDK_ARRAY_TYPES) {
hints
.reflection()
.registerType(TypeReference.of(arrayType));
}
}
}
Loading
Loading