Skip to content

Commit 05c0344

Browse files
committed
Implement filters endpoint
1 parent a8cf46a commit 05c0344

23 files changed

Lines changed: 528 additions & 207 deletions

CHANGELOG.md

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,15 @@
1+
# v6.2.0
2+
3+
## Improvements
4+
- Filter usernames and session tokens in uploaded logs as defined by the instance filters
5+
- Fix some requests hanging when their body is being read
6+
- Trim beginning of logs before applying limits to avoid counting leading newlines and whitespace towards the limit
7+
8+
## Deprecations
9+
- The pattern constants in `Log` are now deprecated and only serve as fallbacks when the filters endpoint is not available on the specified instance.
10+
- `Log#getContent(Limits)` is now deprecated. The new `getContent(FilterList)` method is internal and might change without notice.
11+
- `LogReader#readContents(Limits)` is now deprecated. The new `readContents(FilterList)` method is internal and might change without notice.
12+
113
# v6.1.0
214

315
## Improvements

src/jmh/java/gs/mlco/api/LimitedReaderBenchmark.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,7 @@ public int testWithBufferedInputStreamLineLimit() throws IOException {
5151
}
5252

5353
private int test(Reader in, Integer byteLimit, Integer lineLimit) throws IOException {
54-
try (Reader reader = new LimitedReader(in, new Limits(1, byteLimit, lineLimit))) {
54+
try (Reader reader = new LimitedReader(in, byteLimit, lineLimit, true)) {
5555
StringWriter writer = new StringWriter();
5656
reader.transferTo(writer);
5757
return writer.toString().length();

src/main/java/gs/mclo/api/Instance.java

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -93,6 +93,14 @@ public String getLogUrl(String id) {
9393
return apiBaseUrl + "1/log/" + id;
9494
}
9595

96+
/**
97+
* Get the URL for fetching available filters
98+
* @return the URL for fetching available filters (e.g. <a href="https://api.mclo.gs/1/filters">https://api.mclo.gs/1/filters</a>)
99+
*/
100+
public String getFiltersUrl() {
101+
return apiBaseUrl + "1/filters";
102+
}
103+
96104
private String ensureEndsWithSlash(String url) {
97105
if (!url.endsWith("/"))
98106
url += "/";

src/main/java/gs/mclo/api/Log.java

Lines changed: 30 additions & 69 deletions
Original file line numberDiff line numberDiff line change
@@ -1,28 +1,32 @@
11
package gs.mclo.api;
22

33
import gs.mclo.api.data.Metadata;
4+
import gs.mclo.api.internal.filter.*;
45
import gs.mclo.api.reader.FileLogReader;
56
import gs.mclo.api.reader.LogReader;
67
import gs.mclo.api.reader.StringLogReader;
78
import gs.mclo.api.response.Limits;
9+
import org.jetbrains.annotations.ApiStatus;
810
import org.jetbrains.annotations.Nullable;
911

1012
import java.io.IOException;
1113
import java.nio.file.Path;
14+
import java.util.Arrays;
1215
import java.util.HashSet;
1316
import java.util.Set;
14-
import java.util.regex.Matcher;
1517
import java.util.regex.Pattern;
1618

1719
public class Log {
1820
/**
1921
* pattern for IPv4 addresses
2022
*/
23+
@Deprecated
2124
public static final Pattern IPV4_PATTERN = Pattern.compile("(?<!([0-9]|-|\\w))(?:[1-2]?[0-9]{1,2}\\.){3}[1-2]?[0-9]{1,2}(?!([0-9]|-|\\w))");
2225

2326
/**
2427
* whitelist patterns for IPv4 addresses
2528
*/
29+
@Deprecated
2630
public static final Pattern[] IPV4_FILTER = new Pattern[]{
2731
Pattern.compile("127\\.[0-9]{1,3}\\.[0-9]{1,3}\\.[0-9]{1,3}"),
2832
Pattern.compile("0\\.0\\.0\\.0"),
@@ -33,11 +37,13 @@ public class Log {
3337
/**
3438
* pattern for IPv6 addresses
3539
*/
40+
@Deprecated
3641
public static final Pattern IPV6_PATTERN = Pattern.compile("(?<!([0-9]|-|\\w))(?:[0-9a-f]{0,4}:){7}[0-9a-f]{0,4}(?!([0-9]|-|\\w))", Pattern.CASE_INSENSITIVE);
3742

3843
/**
3944
* whitelist patterns for IPv4 addresses
4045
*/
46+
@Deprecated
4147
public static final Pattern[] IPV6_FILTER = new Pattern[]{
4248
Pattern.compile("[0:]+1?"),
4349
};
@@ -87,84 +93,33 @@ public Log(LogReader reader) {
8793
}
8894

8995
/**
90-
* remove IP addresses
91-
*/
92-
private String filter(String content) {
93-
content = this.filterIPv4(content);
94-
return this.filterIPv6(content);
95-
}
96-
97-
/**
98-
* remove IPv4 addresses
99-
*/
100-
private String filterIPv4(String content) {
101-
Matcher matcher = IPV4_PATTERN.matcher(content);
102-
StringBuilder sb = new StringBuilder();
103-
while (matcher.find()) {
104-
if (isWhitelistedIPv4(matcher.group())) {
105-
continue;
106-
}
107-
matcher.appendReplacement(sb, "**.**.**.**");
108-
}
109-
matcher.appendTail(sb);
110-
return sb.toString();
111-
}
112-
113-
/**
114-
* does this IPv4 address match any whitelist filters
115-
*
116-
* @param s string to test
117-
* @return matches
118-
*/
119-
private boolean isWhitelistedIPv4(String s) {
120-
for (Pattern filter : IPV4_FILTER) {
121-
if (s.matches(filter.pattern())) {
122-
return true;
123-
}
124-
}
125-
return false;
126-
}
127-
128-
/**
129-
* remove IPv6 addresses
96+
* @return log content
13097
*/
131-
private String filterIPv6(String content) {
132-
Matcher matcher = IPV6_PATTERN.matcher(content);
133-
StringBuilder sb = new StringBuilder();
134-
while (matcher.find()) {
135-
if (isWhitelistedIPv6(matcher.group())) {
136-
continue;
137-
}
138-
matcher.appendReplacement(sb, "****:****:****:****:****:****:****:****");
139-
}
140-
matcher.appendTail(sb);
141-
return sb.toString();
142-
}
143-
144-
/**
145-
* does this IPv6 address match any whitelist filters
146-
*
147-
* @param s string to test
148-
* @return matches
149-
*/
150-
private boolean isWhitelistedIPv6(String s) {
151-
for (Pattern filter : IPV6_FILTER) {
152-
if (s.matches(filter.pattern())) {
153-
return true;
154-
}
155-
}
156-
return false;
98+
@Deprecated
99+
public String getContent(Limits limits) throws IOException {
100+
return getContent(new FilterList(new Filter[]{
101+
new TrimFilter(),
102+
new LimitBytesFilter(limits.getMaxLength()),
103+
new LimitLinesFilter(limits.getMaxLines()),
104+
legacyRegex(IPV4_PATTERN, IPV4_FILTER, "**.**.**.**"),
105+
legacyRegex(IPV6_PATTERN, IPV6_FILTER, "****:****:****:****:****:****:****:****"),
106+
}));
157107
}
158108

159109
/**
160110
* @return log content
161111
*/
162-
public String getContent(Limits limits) throws IOException {
112+
@ApiStatus.Internal
113+
public String getContent(FilterList filters) throws IOException {
163114
if (content != null) {
164115
return content;
165116
}
166117

167-
this.content = this.filter(reader.readContents(limits));
118+
content = reader.readContents(filters);
119+
for (var filter : filters.getFilters()) {
120+
content = filter.apply(content);
121+
}
122+
168123
return content;
169124
}
170125

@@ -218,4 +173,10 @@ public Log addMetadata(Metadata<?> data) {
218173
this.metadata.add(data);
219174
return this;
220175
}
176+
177+
private RegexFilter legacyRegex(Pattern pattern, Pattern[] exemptions, String replacement) {
178+
return new RegexFilter(new ReplacingRegexPattern[]{
179+
new ReplacingRegexPattern(pattern.pattern(), new char[]{}, replacement),
180+
}, Arrays.stream(exemptions).map(p -> new RegexPattern(p.pattern(), new char[]{})).toArray(RegexPattern[]::new));
181+
}
221182
}

src/main/java/gs/mclo/api/MclogsClient.java

Lines changed: 40 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -5,12 +5,11 @@
55
import gs.mclo.api.data.LogField;
66
import gs.mclo.api.internal.JsonBodyHandler;
77
import gs.mclo.api.internal.RequestBuilder;
8+
import gs.mclo.api.internal.filter.*;
9+
import gs.mclo.api.internal.gson.FilterTypeAdapterFactory;
810
import gs.mclo.api.internal.gson.InstantTypeAdapter;
911
import gs.mclo.api.reader.FileLogReader;
10-
import gs.mclo.api.response.GetLogResponse;
11-
import gs.mclo.api.response.InsightsResponse;
12-
import gs.mclo.api.response.Limits;
13-
import gs.mclo.api.response.UploadLogResponse;
12+
import gs.mclo.api.response.*;
1413
import org.jetbrains.annotations.ApiStatus;
1514
import org.jetbrains.annotations.Nullable;
1615

@@ -27,6 +26,7 @@
2726
public class MclogsClient {
2827
private final Gson gson = new GsonBuilder()
2928
.registerTypeAdapter(Instant.class, new InstantTypeAdapter())
29+
.registerTypeAdapterFactory(new FilterTypeAdapterFactory())
3030
.create();
3131
private final HttpClient httpClient = HttpClient.newBuilder()
3232
.version(HttpClient.Version.HTTP_2)
@@ -35,6 +35,7 @@ public class MclogsClient {
3535

3636
private Instance instance = new Instance();
3737
private @Nullable Limits limits;
38+
private @Nullable FilterList filters;
3839
private final RequestBuilder requestBuilder = new RequestBuilder(gson);
3940

4041
/**
@@ -143,13 +144,13 @@ public MclogsClient setInstance(Instance instance) {
143144
* @return the response
144145
*/
145146
public CompletableFuture<UploadLogResponse> uploadLog(Log log) {
146-
return getLimitsOrDefault().thenCompose(limits -> {
147+
return getFiltersOrDefault().thenCompose(filters -> {
147148
if (log.getSource() == null) {
148149
log.setSource(requestBuilder.getProjectName());
149150
}
150151

151152
try {
152-
HttpRequest request = requestBuilder.uploadRequest(instance.getLogUploadUrl(), log, limits);
153+
HttpRequest request = requestBuilder.uploadRequest(instance.getLogUploadUrl(), log, filters);
153154
return asyncRequest(request, UploadLogResponse.class);
154155
} catch (IOException e) {
155156
return CompletableFuture.failedFuture(e);
@@ -211,9 +212,9 @@ public CompletableFuture<InsightsResponse> getInsights(String logId) {
211212
* @return the insights of the log
212213
*/
213214
public CompletableFuture<InsightsResponse> analyseLog(Log log) {
214-
return this.getLimitsOrDefault().thenCompose(limits -> {
215+
return this.getFiltersOrDefault().thenCompose(filters -> {
215216
try {
216-
HttpRequest request = requestBuilder.uploadRequest(instance.getLogAnalysisUrl(), log, limits);
217+
HttpRequest request = requestBuilder.uploadRequest(instance.getLogAnalysisUrl(), log, filters);
217218
return asyncRequest(request, InsightsResponse.class);
218219
} catch (IOException e) {
219220
return CompletableFuture.failedFuture(e);
@@ -298,6 +299,29 @@ public CompletableFuture<Void> deleteLog(String id, String token) {
298299
});
299300
}
300301

302+
/**
303+
* Get the filters that should be applied when uploading logs
304+
*
305+
* @return an array of filters
306+
*/
307+
CompletableFuture<FilterList> getFilters() {
308+
var filters = this.filters;
309+
if (filters != null) {
310+
return CompletableFuture.completedFuture(filters);
311+
}
312+
313+
HttpRequest request = requestBuilder.request(instance.getFiltersUrl())
314+
.header("Accept", "application/json")
315+
.GET()
316+
.build();
317+
return asyncRequest(request, Filter[].class)
318+
.thenApply(FilterList::new)
319+
.thenApply(result -> {
320+
this.filters = result;
321+
return result;
322+
});
323+
}
324+
301325
/**
302326
* List logs in the {@code logs} subdirectory of a path
303327
*
@@ -330,6 +354,7 @@ public String[] listCrashReportsInDirectory(Path directory) {
330354

331355
/**
332356
* List all files in a directory that match the allowed file name pattern
357+
*
333358
* @param directory directory to list files from
334359
* @return array of file names that can be uploaded
335360
*/
@@ -346,6 +371,7 @@ public String[] listFilesInDirectory(File directory) {
346371

347372
/**
348373
* List all files in a directory that match the allowed file name pattern
374+
*
349375
* @param directory directory to list files from
350376
* @return array of file names that can be uploaded
351377
*/
@@ -368,8 +394,12 @@ private <T> CompletableFuture<T> asyncRequest(HttpRequest request, Class<T> resp
368394
.thenApply(HttpResponse::body);
369395
}
370396

371-
private CompletableFuture<Limits> getLimitsOrDefault() {
372-
return this.getLimits().exceptionally(t -> Limits.DEFAULT);
397+
private CompletableFuture<FilterList> getFiltersOrDefault() {
398+
return this.getFilters().exceptionally(t -> new FilterList(new Filter[]{
399+
new TrimFilter(),
400+
new LimitBytesFilter(Limits.DEFAULT.getMaxLength()),
401+
new LimitLinesFilter(Limits.DEFAULT.getMaxLines())
402+
}));
373403
}
374404

375405
@ApiStatus.Internal

src/main/java/gs/mclo/api/internal/JsonBodyHandler.java

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,13 @@
11
package gs.mclo.api.internal;
22

33
import com.google.gson.JsonElement;
4+
import com.google.gson.JsonParser;
45
import gs.mclo.api.APIException;
56
import gs.mclo.api.MclogsClient;
67
import org.jetbrains.annotations.Nullable;
78

89
import java.net.http.HttpResponse;
10+
import java.nio.charset.StandardCharsets;
911
import java.util.concurrent.CompletionException;
1012

1113
public final class JsonBodyHandler<T> implements HttpResponse.BodyHandler<T> {
@@ -19,15 +21,17 @@ public JsonBodyHandler(MclogsClient client, Class<T> clazz) {
1921

2022
@Override
2123
public HttpResponse.BodySubscriber<@Nullable T> apply(HttpResponse.ResponseInfo responseInfo) {
22-
return HttpResponse.BodySubscribers.mapping(new JsonElementBodySubscriber(client.gson()), x -> map(x, responseInfo));
24+
return HttpResponse.BodySubscribers.mapping(HttpResponse.BodySubscribers.ofString(StandardCharsets.UTF_8), x -> map(x, responseInfo));
2325
}
2426

25-
@Nullable T map(JsonElement element, HttpResponse.ResponseInfo responseInfo) {
26-
if (!element.isJsonObject()) {
27+
@Nullable T map(String json, HttpResponse.ResponseInfo responseInfo) {
28+
var element = JsonParser.parseString(json);
29+
if (element.isJsonObject()) {
30+
checkError(element, responseInfo);
31+
} else if (!element.isJsonArray()) {
2732
throw new CompletionException(new APIException("Invalid API response (Status code: " + responseInfo.statusCode() + ")", responseInfo.statusCode()));
2833
}
2934

30-
checkError(element, responseInfo);
3135

3236
if (clazz == Void.class) {
3337
return null;

0 commit comments

Comments
 (0)