|
| 1 | +package javasabr.mqtt.acl.mug.dsl.parser; |
| 2 | + |
| 3 | +import com.google.common.labs.parse.Parser; |
| 4 | +import com.google.mu.util.CharPredicate; |
| 5 | +import java.util.List; |
| 6 | +import java.util.function.BiFunction; |
| 7 | +import java.util.stream.Collectors; |
| 8 | +import javasabr.mqtt.acl.engine.exception.AclConfigurationException; |
| 9 | +import javasabr.mqtt.acl.engine.model.condition.AllOfCondition; |
| 10 | +import javasabr.mqtt.acl.engine.model.condition.AnyOfCondition; |
| 11 | +import javasabr.mqtt.acl.engine.model.condition.ClientIdCondition; |
| 12 | +import javasabr.mqtt.acl.engine.model.condition.IpAddressCondition; |
| 13 | +import javasabr.mqtt.acl.engine.model.condition.MqttUserCondition; |
| 14 | +import javasabr.mqtt.acl.engine.model.condition.TopicCondition; |
| 15 | +import javasabr.mqtt.acl.engine.model.condition.UserNameCondition; |
| 16 | +import javasabr.mqtt.acl.engine.model.matcher.TopicFilterMatcher; |
| 17 | +import javasabr.mqtt.acl.engine.model.matcher.TopicMatcher; |
| 18 | +import javasabr.mqtt.acl.engine.model.matcher.TopicNameMatcher; |
| 19 | +import javasabr.mqtt.acl.engine.model.matcher.UserMatchers; |
| 20 | +import javasabr.mqtt.acl.engine.model.matcher.ValueMatcher; |
| 21 | +import javasabr.mqtt.acl.engine.model.matcher.dynamic.DynamicTopicMatcher; |
| 22 | +import javasabr.mqtt.acl.engine.model.rule.AclRule; |
| 23 | +import javasabr.mqtt.acl.engine.model.rule.AllowPublishAclRule; |
| 24 | +import javasabr.mqtt.acl.engine.model.rule.AllowSubscribeAclRule; |
| 25 | +import javasabr.mqtt.acl.engine.model.rule.DenyPublishAclRule; |
| 26 | +import javasabr.mqtt.acl.engine.model.rule.DenySubscribeAclRule; |
| 27 | +import javasabr.mqtt.model.topic.TopicFilter; |
| 28 | +import javasabr.mqtt.model.topic.TopicName; |
| 29 | +import javasabr.mqtt.model.topic.TopicValidator; |
| 30 | +import javasabr.rlib.collections.array.Array; |
| 31 | +import javasabr.rlib.collections.array.ArrayBuilder; |
| 32 | +import javasabr.rlib.collections.array.ArrayCollectors; |
| 33 | +import lombok.AccessLevel; |
| 34 | +import lombok.experimental.FieldDefaults; |
| 35 | + |
| 36 | +@FieldDefaults(level = AccessLevel.PRIVATE, makeFinal = true) |
| 37 | +public class GaclParser { |
| 38 | + |
| 39 | + CharPredicate whitespace = CharPredicate.anyOf(" \t\r\n"); |
| 40 | + |
| 41 | + Parser<String> string = Parser.anyOf( |
| 42 | + Parser.quotedByWithEscapes('"', '"', Parser.chars(1)), |
| 43 | + Parser.quotedByWithEscapes('\'', '\'', Parser.chars(1))); |
| 44 | + |
| 45 | + Parser<ValueMatcher<String>> userMatcher = Parser.anyOf( |
| 46 | + Parser |
| 47 | + .word("startsWith") |
| 48 | + .then(string.between("(", ")")) |
| 49 | + .map(UserMatchers::startsWith), |
| 50 | + Parser |
| 51 | + .word("contains") |
| 52 | + .then(string.between("(", ")")) |
| 53 | + .map(UserMatchers::contains), |
| 54 | + Parser |
| 55 | + .word("eq") |
| 56 | + .then(string.between("(", ")")) |
| 57 | + .map(UserMatchers::eq), |
| 58 | + Parser |
| 59 | + .word("regex") |
| 60 | + .then(string.between("(", ")")) |
| 61 | + .map(UserMatchers::regex), |
| 62 | + Parser |
| 63 | + .word("anyValue") |
| 64 | + .followedBy("()") |
| 65 | + .thenReturn(ValueMatcher.MATCH_ANY_STRING)); |
| 66 | + |
| 67 | + Parser<String> identityType = Parser.anyOf( |
| 68 | + Parser.word("userName"), |
| 69 | + Parser.word("clientId"), |
| 70 | + Parser.word("ipAddress")); |
| 71 | + |
| 72 | + Parser<String> identityBlockType = Parser.anyOf( |
| 73 | + Parser.word("userNames"), |
| 74 | + Parser.word("clientIds"), |
| 75 | + Parser.word("ipAddresses")); |
| 76 | + |
| 77 | + Parser<List<MqttUserCondition>> userCondition = Parser.define(uc -> Parser.<List<MqttUserCondition>>anyOf( |
| 78 | + Parser |
| 79 | + .word("anyUser") |
| 80 | + .followedBy("()") |
| 81 | + .thenReturn(List.of(MqttUserCondition.MATCH_ANY)), |
| 82 | + Parser.sequence(identityType, userMatcher, (id, matcher) -> List.of(toUserCondition(id, matcher))), |
| 83 | + Parser.sequence( |
| 84 | + identityBlockType, |
| 85 | + userMatcher |
| 86 | + .atLeastOnce() |
| 87 | + .between("{", "}"), |
| 88 | + (id, matchers) -> matchers |
| 89 | + .stream() |
| 90 | + .map(matcher -> toUserCondition(id, matcher)) |
| 91 | + .collect(Collectors.toList())), |
| 92 | + Parser |
| 93 | + .word("allOf") |
| 94 | + .then(uc |
| 95 | + .atLeastOnce() |
| 96 | + .between("{", "}") |
| 97 | + .map(lists -> { |
| 98 | + Array<MqttUserCondition> flat = lists |
| 99 | + .stream() |
| 100 | + .flatMap(List::stream) |
| 101 | + .collect(ArrayCollectors.toArray(MqttUserCondition.class)); |
| 102 | + return List.of(new AllOfCondition(flat)); |
| 103 | + })), |
| 104 | + Parser |
| 105 | + .word("anyOf") |
| 106 | + .then(uc |
| 107 | + .atLeastOnce() |
| 108 | + .between("{", "}") |
| 109 | + .map(lists -> { |
| 110 | + Array<MqttUserCondition> flat = lists |
| 111 | + .stream() |
| 112 | + .flatMap(List::stream) |
| 113 | + .collect(ArrayCollectors.toArray(MqttUserCondition.class)); |
| 114 | + return List.of(new AnyOfCondition(flat)); |
| 115 | + })))); |
| 116 | + |
| 117 | + Parser<MqttUserCondition> usersSection = Parser |
| 118 | + .word("users") |
| 119 | + .then(userCondition |
| 120 | + .atLeastOnce() |
| 121 | + .between("{", "}") |
| 122 | + .map(lists -> { |
| 123 | + Array<MqttUserCondition> flat = lists |
| 124 | + .stream() |
| 125 | + .flatMap(List::stream) |
| 126 | + .collect(ArrayCollectors.toArray(MqttUserCondition.class)); |
| 127 | + if (flat.isEmpty()) { |
| 128 | + return MqttUserCondition.MATCH_NONE; |
| 129 | + } |
| 130 | + if (flat.size() == 1) { |
| 131 | + return flat.get(0); |
| 132 | + } |
| 133 | + return new AnyOfCondition(flat); |
| 134 | + })); |
| 135 | + |
| 136 | + Parser<TopicMatcher> topicMatcher = Parser.anyOf( |
| 137 | + Parser |
| 138 | + .word("eq") |
| 139 | + .then(string.between("(", ")")) |
| 140 | + .map(s -> { |
| 141 | + if (!TopicValidator.validateTopicName(s)) { |
| 142 | + throw new AclConfigurationException("Invalid topic name:[%s]".formatted(s)); |
| 143 | + } |
| 144 | + return (TopicMatcher) new TopicNameMatcher(TopicName.valueOf(s)); |
| 145 | + }), |
| 146 | + Parser |
| 147 | + .word("match") |
| 148 | + .then(string.between("(", ")")) |
| 149 | + .map(s -> { |
| 150 | + if (!TopicValidator.validateTopicFilter(s)) { |
| 151 | + throw new AclConfigurationException("Invalid topic filter:[%s]".formatted(s)); |
| 152 | + } |
| 153 | + return (TopicMatcher) new TopicFilterMatcher(TopicFilter.valueOf(s)); |
| 154 | + }), |
| 155 | + Parser |
| 156 | + .word("dynamic") |
| 157 | + .then(string.between("(", ")")) |
| 158 | + .map(s -> { |
| 159 | + try { |
| 160 | + return DynamicTopicMatcher.autoBuild(s); |
| 161 | + } catch (RuntimeException e) { |
| 162 | + throw new AclConfigurationException("Invalid dynamic topic pattern", e); |
| 163 | + } |
| 164 | + }), |
| 165 | + Parser |
| 166 | + .word("anyTopic") |
| 167 | + .followedBy("()") |
| 168 | + .thenReturn(TopicMatcher.MATCH_ANY)); |
| 169 | + |
| 170 | + Parser<Array<TopicMatcher>> topicsSection = Parser |
| 171 | + .word("topics") |
| 172 | + .then(topicMatcher |
| 173 | + .atLeastOnce() |
| 174 | + .between("{", "}") |
| 175 | + .map(matchers -> new ArrayBuilder<>(TopicMatcher.class).add(matchers)) |
| 176 | + .map(ArrayBuilder::build)); |
| 177 | + |
| 178 | + Parser<AclRule> directive = Parser.anyOf( |
| 179 | + createDirective("allowPublish", AllowPublishAclRule::new), |
| 180 | + createDirective("denyPublish", DenyPublishAclRule::new), |
| 181 | + createDirective("allowSubscribe", AllowSubscribeAclRule::new), |
| 182 | + createDirective("denySubscribe", DenySubscribeAclRule::new)); |
| 183 | + |
| 184 | + public List<AclRule> parse(String input) { |
| 185 | + try { |
| 186 | + return directive |
| 187 | + .atLeastOnce() |
| 188 | + .parseSkipping(whitespace, input); |
| 189 | + } catch (Parser.ParseException e) { |
| 190 | + throw new AclConfigurationException("Syntax error at %s".formatted(toLineColumn(input, e.getSourceIndex()))); |
| 191 | + } |
| 192 | + } |
| 193 | + |
| 194 | + private Parser<AclRule> createDirective( |
| 195 | + String keyword, |
| 196 | + BiFunction<MqttUserCondition, TopicCondition, AclRule> factory) { |
| 197 | + return Parser |
| 198 | + .word(keyword) |
| 199 | + .then(Parser |
| 200 | + .sequence(usersSection, topicsSection, (uc, tm) -> factory.apply(uc, new TopicCondition(tm))) |
| 201 | + .between("{", "}")); |
| 202 | + } |
| 203 | + |
| 204 | + private MqttUserCondition toUserCondition(String identityType, ValueMatcher<String> matcher) { |
| 205 | + return switch (identityType) { |
| 206 | + case "userName", "userNames" -> new UserNameCondition(matcher); |
| 207 | + case "clientId", "clientIds" -> new ClientIdCondition(matcher); |
| 208 | + case "ipAddress", "ipAddresses" -> new IpAddressCondition(matcher); |
| 209 | + default -> throw new AclConfigurationException("Unknown identity type: %s".formatted(identityType)); |
| 210 | + }; |
| 211 | + } |
| 212 | + |
| 213 | + private String toLineColumn(String input, int index) { |
| 214 | + int line = 1; |
| 215 | + int col = 1; |
| 216 | + for (int i = 0; i < index && i < input.length(); i++) { |
| 217 | + if (input.charAt(i) == '\n') { |
| 218 | + line++; |
| 219 | + col = 1; |
| 220 | + } else { |
| 221 | + col++; |
| 222 | + } |
| 223 | + } |
| 224 | + return "line %s, column %s".formatted(line, col); |
| 225 | + } |
| 226 | +} |
0 commit comments