Skip to content
Merged
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
16 changes: 16 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,22 @@ List<RecogMatchResult> matchResults = recog.fingerprint("Apache HTTPD 6.5");
// draw the rest of the owl...
```

#### Configuring Pattern Matching

By default, recog-java uses Java's standard regular expression package, `java.util.regex`. To use a different implementation, users can implement their own `RecogPatternMatcher` instance:

```java
import com.rapid7.recog.pattern.RecogPatternMatcher;

public class CustomPatternMatcher implements RecogPatternMatcher {
// custom implementation...
}

RecogPatternMatcher patternMatcher = new CustomPatternMatcher("^Apache HTTPD (?<version>.*)$");
RecogMatcher matcher = new RecogMatcher(patternMatcher);
Map<String, String> results = matcher.match("Apache HTTPD 6.5");
```

## Differences from Ruby implementation

This library is not yet at a 1:1 parity with the original [rapid7/recog](https://github.com/rapid7/recog) Ruby implementation.
Expand Down
46 changes: 31 additions & 15 deletions src/main/java/com/rapid7/recog/RecogMatcher.java
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
package com.rapid7.recog;

import com.rapid7.recog.pattern.JavaRegexRecogPatternMatcher;
import com.rapid7.recog.pattern.RecogPatternMatchResult;
import com.rapid7.recog.pattern.RecogPatternMatcher;
import java.io.Serializable;
import java.util.HashMap;
import java.util.HashSet;
Expand All @@ -21,8 +24,7 @@
*/
public class RecogMatcher implements Serializable {

/** The regular expression pattern to match. */
private Pattern pattern;
private final RecogPatternMatcher matcher;

/** "Constant" values always matched as parameters. Key is the name, value is the value. */
private Map<String, String> values;
Expand All @@ -45,8 +47,23 @@ public class RecogMatcher implements Serializable {
/** Optional examples that illustrate the matcher (or that can be used to test the matcher). */
private Set<String> examples;

/**
* Creates a new RecogMatcher using a {@link JavaRegexRecogPatternMatcher} to
* match fingerprint values.
*
* @param pattern The regular expression pattern to match fingerprint values against.
*/
public RecogMatcher(Pattern pattern) {
this.pattern = requireNonNull(pattern);
this(new JavaRegexRecogPatternMatcher(pattern));
}

/**
* Creates a RecogMatcher with the specified {@link RecogPatternMatcher}.
*
* @param matcher The {@link RecogPatternMatcher} to use when matching fingerprint values.
*/
public RecogMatcher(RecogPatternMatcher matcher) {
this.matcher = matcher;
values = new HashMap<>();
positionalParameters = new HashMap<>();
namedParameters = new HashSet<>();
Expand Down Expand Up @@ -111,7 +128,7 @@ public boolean matches(String input) {
if (input == null)
return false;
else
return pattern.matcher(input).find();
return matcher.matches(input);
}

/**
Expand All @@ -129,19 +146,19 @@ public Map<String, String> match(String input) {
if (input == null)
return null;

Matcher matcher = pattern.matcher(input);
if (matcher.find()) {
RecogPatternMatchResult result = matcher.match(input);
if (result != null) {
Map<String, String> values = new HashMap<>();
values.putAll(this.values);

// parse positional parameters for the groups specified
for (Entry<String, Integer> parameter : positionalParameters.entrySet())
if (parameter.getValue() <= matcher.groupCount())
values.put(parameter.getKey(), matcher.group(parameter.getValue()));
if (parameter.getValue() <= result.groupCount())
values.put(parameter.getKey(), result.group(parameter.getValue()));

for (String parameter : namedParameters) {
try {
values.put(parameter, matcher.group(parameter));
values.put(parameter, result.group(parameter));
} catch (IllegalArgumentException exception) {
// the group with the name doesn't exist, ignore it
}
Expand Down Expand Up @@ -199,7 +216,7 @@ public RecogMatcher addParam(String name) {
}

public String getPattern() {
return pattern.pattern();
return matcher.getPattern();
}

/**
Expand All @@ -221,9 +238,9 @@ public static Pattern pattern(String regex, int... flags) {
@Override
public String toString() {
return new StringJoiner(", ", RecogMatcher.class.getSimpleName() + "[", "]")
.add("Pattern=" + pattern.pattern())
.add("Pattern=" + matcher.getPattern())
.add("Description=" + description)
.add("Flags=" + pattern.flags())
.add("Flags=" + matcher.getFlags())
.add("Positional Parameters=" + positionalParameters)
.add("Named Parameters=" + namedParameters)
.add("Values=" + values)
Expand All @@ -233,7 +250,7 @@ public String toString() {

@Override
public int hashCode() {
return Objects.hash(pattern, values, positionalParameters);
return Objects.hash(matcher, values, positionalParameters);
}

@Override
Expand All @@ -244,8 +261,7 @@ else if (!(obj instanceof RecogMatcher))
return false;
else {
RecogMatcher other = (RecogMatcher) obj;
return Objects.equals(pattern.flags(), other.pattern.flags())
&& Objects.equals(pattern.pattern(), other.pattern.pattern())
return Objects.equals(matcher, other.matcher)
&& Objects.equals(values, other.values)
&& Objects.equals(positionalParameters, other.positionalParameters)
&& Objects.equals(namedParameters, other.namedParameters);
Expand Down
32 changes: 31 additions & 1 deletion src/main/java/com/rapid7/recog/parser/RecogParser.java
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@

import com.rapid7.recog.RecogMatcher;
import com.rapid7.recog.RecogMatchers;
import com.rapid7.recog.pattern.JavaRegexRecogPatternMatcher;
import com.rapid7.recog.pattern.RecogPatternMatcher;
import java.io.File;
import java.io.FileReader;
import java.io.IOException;
Expand All @@ -27,8 +29,24 @@
*/
public class RecogParser {

/**
* Factory used to create the underlying {@link RecogPatternMatcher} used
* when matching inputs against regular expressions.
*/
public interface PatternMatcherFactory {
RecogPatternMatcher create(String pattern, int flags);
}

/**
* The default {@link PatternMatcherFactory} uses java.regex.* packages to evaluate
* regular expressions.
*/
public static final PatternMatcherFactory DEFAULT_PATTERN_MATCHER_FACTORY =
(pattern, flags) -> new JavaRegexRecogPatternMatcher(Pattern.compile(pattern, flags));

private static final Logger LOGGER = LoggerFactory.getLogger(RecogParser.class);
private final boolean strictMode;
private final PatternMatcherFactory patternMatcherFactory;

/**
* Constructs a parser to parser with non-strict (lenient) parsing mode.
Expand All @@ -44,7 +62,19 @@ public RecogParser() {
* encountered, {@code false} otherwise.
*/
public RecogParser(boolean strictMode) {
this(strictMode, DEFAULT_PATTERN_MATCHER_FACTORY);
}

/**
* Constructs a parser with the specified strictness mode and {@link PatternMatcherFactory}.
*
* @param strictMode {@code true} if the parser should throw exceptions when any error is
* encountered, {@code false} otherwise.
* @param patternMatcherFactory The {@link PatternMatcherFactory} to be used during parsing.
*/
public RecogParser(boolean strictMode, PatternMatcherFactory patternMatcherFactory) {
this.strictMode = strictMode;
this.patternMatcherFactory = patternMatcherFactory;
}

/**
Expand Down Expand Up @@ -117,7 +147,7 @@ public RecogMatchers parse(Reader reader, String name)
int regexFlags = parseFlags(fingerprint.getAttribute("flags"));

// construct a pattern
RecogMatcher fingerprintPattern = new RecogMatcher(Pattern.compile(pattern, regexFlags));
RecogMatcher fingerprintPattern = new RecogMatcher(patternMatcherFactory.create(pattern, regexFlags));

// description (optional)
NodeList description = fingerprint.getElementsByTagName("description");
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
package com.rapid7.recog.pattern;

import java.util.Objects;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import static java.util.Objects.requireNonNull;

/**
* An implementation of {@link RecogPatternMatcher} that uses java.util.regex.*
* packages to match fingerprint values against fingerprint patterns.
* Matching of the patterns specified is performed using a sub-sequence or "partial"
* match. See {@link Matcher#find()} vs {@link Matcher#matches()}.
*/
public class JavaRegexRecogPatternMatcher implements RecogPatternMatcher {

private static class JavaRegexRecogPatternMatchResult implements RecogPatternMatchResult {
private final Matcher matcher;

JavaRegexRecogPatternMatchResult(Matcher matcher) {
this.matcher = matcher;
}

@Override
public int groupCount() {
return matcher.groupCount();
}

@Override
public String group(int group) {
return matcher.group(group);
}

@Override
public String group(String group) {
return matcher.group(group);
}
}

/**
* The regular expression pattern to match.
*/
private final Pattern pattern;

public JavaRegexRecogPatternMatcher(Pattern pattern) {
this.pattern = requireNonNull(pattern);
}

@Override
public String getPattern() {
return pattern.pattern();
}

@Override
public int getFlags() {
return pattern.flags();
}

@Override
public boolean matches(String input) {
return input != null && pattern.matcher(input).find();
}

@Override
public RecogPatternMatchResult match(String input) {
if (input == null) {
return null;
}
Matcher matcher = pattern.matcher(input);
return matcher.find() ? new JavaRegexRecogPatternMatchResult(matcher) : null;
}

@Override
public boolean equals(Object other) {
if (this == other) {
return true;
} else if (!(other instanceof JavaRegexRecogPatternMatcher)) {
return false;
} else {
JavaRegexRecogPatternMatcher that = (JavaRegexRecogPatternMatcher) other;
return Objects.equals(getPattern(), that.getPattern())
&& Objects.equals(getFlags(), that.getFlags());
}
}

@Override
public int hashCode() {
return Objects.hash(pattern);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
package com.rapid7.recog.pattern;

/**
* The result of a match operation.
*/
public interface RecogPatternMatchResult {

/**
* Returns the number of capturing groups in this result.
*/
int groupCount();

/**
* Returns the input captured by the indexed group.
*
* @param index The index of the capturing group. Group indexes start at one.
* @return The input captured by the group at the specified index, or {@code null}
* if there is no matching input for this group.
* @throws IndexOutOfBoundsException if the index is less than 1 or greater than
* that returned of {@code groupCount()}.
*/
String group(int index);

/**
* Returns the input captured by the named group.
*
* @param name The name of the capturing group.
* @return Input captured by the named group or {@code null} if there is no
* matching input for this group.
* @throws IllegalArgumentException if there is no group with this name.
*/
String group(String name);

}
32 changes: 32 additions & 0 deletions src/main/java/com/rapid7/recog/pattern/RecogPatternMatcher.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
package com.rapid7.recog.pattern;

/**
* Performs matching of input values against a regular expression that supports grouped parameter
* extraction.
*/
public interface RecogPatternMatcher {

/** The regex pattern this matcher matches. */
String getPattern();

int getFlags();

/**
* Returns whether this matcher matches the specified input fingerprint value.
*
* @param input The fingerprint to test this matcher against. May be {@code null}.
* @return {@code true} if the input is non-{@code null} and matches the fingerprint matcher
* pattern.
*/
boolean matches(String input);

/**
* Matches the regular expression against the specified input.
*
* @param input The fingerprint to match. May be {@code null}.
* @return {@code null} if the input does not match the pattern, otherwise a non-{@code null}
* {@link RecogPatternMatchResult}
*/
RecogPatternMatchResult match(String input);

}
Loading