diff --git a/.github/workflows/master-2.yml b/.github/workflows/master-2.yml index 22bf2a5e..e817d615 100644 --- a/.github/workflows/master-2.yml +++ b/.github/workflows/master-2.yml @@ -17,10 +17,10 @@ jobs: with: fetch-depth: 0 - - name: Set up JDK 17 + - name: Set up JDK 11 uses: actions/setup-java@v3 with: - java-version: 17 + java-version: 11 distribution: 'temurin' cache: maven @@ -34,7 +34,7 @@ jobs: strategy: fail-fast: false matrix: - java: [ '17', '18'] + java: [ '11', '17', '18'] os: [ ubuntu-latest, windows-latest ] runs-on: ${{ matrix.os }} if: "! contains(toJSON(github.event.commits.*.message), '[skip ci]')" diff --git a/pom.xml b/pom.xml index e9f2680b..c7a63729 100644 --- a/pom.xml +++ b/pom.xml @@ -46,7 +46,7 @@ UTF-8 UTF-8 - 17 + 11 ${java.version} ${java.version} @@ -166,12 +166,6 @@ junit 4.13.1 - - - com.fasterxml.jackson.core - jackson-databind - 2.14.2 - diff --git a/src/main/java/com/github/switcherapi/client/model/Entry.java b/src/main/java/com/github/switcherapi/client/model/Entry.java index def6a03d..3a3c8fef 100644 --- a/src/main/java/com/github/switcherapi/client/model/Entry.java +++ b/src/main/java/com/github/switcherapi/client/model/Entry.java @@ -60,7 +60,8 @@ public int hashCode() { @Override public boolean equals(Object obj) { - if (obj instanceof Entry entry) { + if (obj instanceof Entry) { + Entry entry = (Entry) obj; if (!this.strategy.equals(entry.getStrategy())) { return false; } diff --git a/src/main/java/com/github/switcherapi/client/service/ValidatorService.java b/src/main/java/com/github/switcherapi/client/service/ValidatorService.java index d676a2ad..352efe9f 100644 --- a/src/main/java/com/github/switcherapi/client/service/ValidatorService.java +++ b/src/main/java/com/github/switcherapi/client/service/ValidatorService.java @@ -26,7 +26,7 @@ private void initializeValidators() { registerValidator(PayloadValidator.class); registerValidator(TimeValidator.class); registerValidator(ValueValidator.class); - registerValidator(RegexValidatorV8.getPlatformValidator()); + registerValidator(RegexValidator.class); } private StrategyValidator getStrategyValidator(Class validatorClass) { diff --git a/src/main/java/com/github/switcherapi/client/service/validators/RegexValidatorV8.java b/src/main/java/com/github/switcherapi/client/service/validators/RegexValidatorV8.java deleted file mode 100644 index d429b72d..00000000 --- a/src/main/java/com/github/switcherapi/client/service/validators/RegexValidatorV8.java +++ /dev/null @@ -1,191 +0,0 @@ -package com.github.switcherapi.client.service.validators; - -import com.github.switcherapi.client.SwitcherContextBase; -import com.github.switcherapi.client.exception.SwitcherException; -import com.github.switcherapi.client.exception.SwitcherInvalidOperationException; -import com.github.switcherapi.client.exception.SwitcherValidatorException; -import com.github.switcherapi.client.model.ContextKey; -import com.github.switcherapi.client.model.Entry; -import com.github.switcherapi.client.model.StrategyValidator; -import com.github.switcherapi.client.model.criteria.Strategy; -import com.github.switcherapi.client.service.WorkerName; -import org.apache.commons.lang3.tuple.Pair; -import org.apache.logging.log4j.LogManager; -import org.apache.logging.log4j.Logger; - -import java.util.Arrays; -import java.util.HashSet; -import java.util.Set; -import java.util.concurrent.*; -import java.util.regex.Matcher; -import java.util.regex.Pattern; - -/** - * Regex Validator for applications running using Java 1.8 - * 'String.matches' has been improved since Java 9, and it is expected be ReDoS-safe - *

- * This implementation allow you to define the sweet-spot process timing when using Regex Strategies - * - * @author Roger Floriano (petruki) - * @since 2023-02-18 - */ -@ValidatorComponent(type = StrategyValidator.REGEX) -public class RegexValidatorV8 extends Validator { - - private static final Logger logger = LogManager.getLogger(RegexValidatorV8.class); - - /** - * Global flag to interrupt workers. - * Should perform better than Thread.currentThread().isInterrupted() - */ - private boolean workerInterrupted; - - private static final String DELIMITER_REGEX = "\\b%s\\b"; - - private final Set> blackList; - private final TimedMatch timedMatch; - - public RegexValidatorV8() { - timedMatch = new TimedMatch(); - blackList = new HashSet<>(); - } - - public static Class getPlatformValidator() { - if (System.getProperty("java.version").startsWith("1.8")) - return RegexValidatorV8.class; - return RegexValidator.class; - } - - @Override - public boolean process(Strategy strategy, Entry switcherInput) throws SwitcherInvalidOperationException { - try { - switch (strategy.getEntryOperation()) { - case EXIST: - return Arrays.stream(strategy.getValues()).anyMatch(val -> { - try { - return timedMatch(switcherInput.getInput(), val); - } catch (TimeoutException | SwitcherValidatorException e) { - logger.error(e); - return false; - } - }); - case NOT_EXIST: - return Arrays.stream(strategy.getValues()).noneMatch(val -> { - try { - return timedMatch(switcherInput.getInput(), val); - } catch (TimeoutException | SwitcherValidatorException e) { - logger.error(e); - return true; - } - }); - case EQUAL: - return strategy.getValues().length == 1 - && timedMatch(switcherInput.getInput(), String.format(DELIMITER_REGEX, strategy.getValues()[0])); - case NOT_EQUAL: - return strategy.getValues().length == 1 - && !timedMatch(switcherInput.getInput(), String.format(DELIMITER_REGEX, strategy.getValues()[0])); - default: - throw new SwitcherInvalidOperationException(strategy.getOperation(), strategy.getStrategy()); - } - } catch (TimeoutException | SwitcherValidatorException e) { - logger.error(e); - return false; - } - } - - private boolean timedMatch(final String input, final String regex) throws TimeoutException { - if (isBlackListed(input, regex)) - throw new SwitcherValidatorException(input, regex); - - timedMatch.init(input, regex); - final ExecutorService executor = Executors.newSingleThreadExecutor(r -> { - Thread thread = new Thread(r); - thread.setName(WorkerName.REGEX_VALIDATOR_WORKER.toString()); - thread.setDaemon(true); - return thread; - }); - - final Future future = executor.submit(timedMatch); - - try { - return future.get(Integer.parseInt(SwitcherContextBase.contextStr(ContextKey.REGEX_TIMEOUT)), - TimeUnit.MILLISECONDS); - } catch (TimeoutException e) { - workerInterrupted = true; - future.cancel(true); - addBlackList(input, regex); - throw new TimeoutException(); - } catch (Exception e) { - Thread.currentThread().interrupt(); - throw new SwitcherValidatorException(input, regex); - } finally { - executor.shutdownNow(); - } - } - - private boolean isBlackListed(final String input, final String regex) { - return blackList.stream().anyMatch(bl -> - regex.equals(bl.getRight()) && bl.getLeft().toLowerCase().contains(input.toLowerCase())); - } - - private void addBlackList(final String input, final String regex) { - blackList.add(Pair.of(input, regex)); - } - - class TimedMatch implements Callable { - private String input; - private String regex; - - public void init(String input, String regex) { - workerInterrupted = false; - this.input = input; - this.regex = regex; - } - - @Override - public Boolean call() { - final Pattern pattern = Pattern.compile(regex); - Matcher matcher = pattern.matcher(new InterruptibleCharSequence(input)); - return matcher.find(); - } - } - - /** - * Credits to Lincoln - * - */ - class InterruptibleCharSequence implements CharSequence { - - private final CharSequence inner; - - public InterruptibleCharSequence(CharSequence inner) { - super(); - this.inner = inner; - } - - @Override - public char charAt(int index) { - if (workerInterrupted) { - throw new SwitcherException("A Switcher SDK thread has been interrupted", null); - } - - return inner.charAt(index); - } - - @Override - public int length() { - return inner.length(); - } - - @Override - public CharSequence subSequence(int start, int end) { - return new InterruptibleCharSequence(inner.subSequence(start, end)); - } - - @Override - public String toString() { - return inner.toString(); - } - } - -} diff --git a/src/test/java/com/github/switcherapi/client/validator/RegexValidatorV8Test.java b/src/test/java/com/github/switcherapi/client/validator/RegexValidatorV8Test.java deleted file mode 100644 index d4ac5321..00000000 --- a/src/test/java/com/github/switcherapi/client/validator/RegexValidatorV8Test.java +++ /dev/null @@ -1,121 +0,0 @@ -package com.github.switcherapi.client.validator; - -import com.github.switcherapi.client.model.Entry; -import com.github.switcherapi.client.model.EntryOperation; -import com.github.switcherapi.client.model.StrategyValidator; -import com.github.switcherapi.client.model.criteria.Strategy; -import com.github.switcherapi.client.service.WorkerName; -import com.github.switcherapi.client.service.validators.RegexValidatorV8; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.condition.EnabledOnJre; -import org.junit.jupiter.api.condition.JRE; -import org.junit.jupiter.params.ParameterizedTest; -import org.junit.jupiter.params.provider.Arguments; -import org.junit.jupiter.params.provider.MethodSource; - -import java.time.Duration; -import java.util.Collections; -import java.util.List; -import java.util.stream.Stream; - -import static org.junit.jupiter.api.Assertions.*; - -@EnabledOnJre(value = { JRE.JAVA_8 }) -class RegexValidatorV8Test { - - private static final String EVIL_REGEX = "^(([a-z])+.)+[A-Z]([a-z])+$"; - private static final String EVIL_INPUT = "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"; - - static Stream evilRegexTestArguments() { - return Stream.of( - Arguments.of(EntryOperation.EXIST, Boolean.FALSE), - Arguments.of(EntryOperation.NOT_EXIST, Boolean.FALSE), - Arguments.of(EntryOperation.EQUAL, Boolean.FALSE), - Arguments.of(EntryOperation.NOT_EQUAL, Boolean.FALSE) - ); - } - - @ParameterizedTest() - @MethodSource("evilRegexTestArguments") - void shouldFailEvilRegexInput(EntryOperation operation, boolean expected) { - //given - RegexValidatorV8 regexValidator = new RegexValidatorV8(); - Strategy strategy = givenStrategy(operation, Collections.singletonList(EVIL_REGEX)); - Entry entry = Entry.build(StrategyValidator.REGEX, EVIL_INPUT); - - //test - boolean actual = assertTimeoutPreemptively(Duration.ofMillis(5000), () -> regexValidator.process(strategy, entry)); - assertEquals(expected, actual); - } - - @Test - void shouldBlackListEvilInput_immediateReturnNextCall() { - //given - RegexValidatorV8 regexValidator = new RegexValidatorV8(); - Strategy strategy = givenStrategy(EntryOperation.EXIST, Collections.singletonList(EVIL_REGEX)); - Entry entry = Entry.build(StrategyValidator.REGEX, EVIL_INPUT); - - //test - boolean result = assertTimeoutPreemptively(Duration.ofMillis(4000), () -> regexValidator.process(strategy, entry)); - assertFalse(result); - - result = assertTimeoutPreemptively(Duration.ofMillis(100), () -> regexValidator.process(strategy, entry)); - assertFalse(result); - } - - @Test - void shouldBlackListEvilInput_immediateReturnNextCall_similarInput() { - //given - RegexValidatorV8 regexValidator = new RegexValidatorV8(); - Strategy strategy = givenStrategy(EntryOperation.EXIST, Collections.singletonList(EVIL_REGEX)); - - //test - Entry entry1 = Entry.build(StrategyValidator.REGEX, "aaaaaaaaaaaaaaaaaaaaaaaaaaaaa"); - boolean result = assertTimeoutPreemptively(Duration.ofMillis(4000), () -> regexValidator.process(strategy, entry1)); - assertFalse(result); - - Entry entry2 = Entry.build(StrategyValidator.REGEX, "bbbbaaaaaaaaaaaaaaa"); - result = assertTimeoutPreemptively(Duration.ofMillis(100), () -> regexValidator.process(strategy, entry2)); - assertFalse(result); - } - - @Test - void shouldFail_nullInput() { - //given - RegexValidatorV8 regexValidator = new RegexValidatorV8(); - Strategy strategy = givenStrategy(EntryOperation.EXIST, Collections.singletonList(EVIL_REGEX)); - Entry entry = Entry.build(StrategyValidator.REGEX, null); - - //test - boolean result = regexValidator.process(strategy, entry); - assertFalse(result); - } - - @Test - void shouldCompleteWorkerThreadsAfterTimeout() { - //given - RegexValidatorV8 regexValidator = new RegexValidatorV8(); - Strategy strategy = givenStrategy(EntryOperation.EXIST, Collections.singletonList(EVIL_REGEX)); - Entry entry = Entry.build(StrategyValidator.REGEX, EVIL_INPUT); - - //test - boolean result = regexValidator.process(strategy, entry); - assertFalse(result); - assertWorkerNotExists(); - } - - private Strategy givenStrategy(EntryOperation operation, List values) { - Strategy strategy = new Strategy(); - strategy.setStrategy(StrategyValidator.REGEX.toString()); - strategy.setOperation(operation.toString()); - strategy.setValues(values.toArray(new String[0])); - - return strategy; - } - - private void assertWorkerNotExists() { - assertFalse(Thread.getAllStackTraces().keySet().stream() - .anyMatch(t -> t.getName().equals(WorkerName.REGEX_VALIDATOR_WORKER.toString()))); - } - -}