org.graylog2.lookup.adapters.HTTPJSONPathDataAdapter Maven / Gradle / Ivy
/*
* Copyright (C) 2020 Graylog, Inc.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the Server Side Public License, version 1,
* as published by MongoDB, Inc.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* Server Side Public License for more details.
*
* You should have received a copy of the Server Side Public License
* along with this program. If not, see
* .
*/
package org.graylog2.lookup.adapters;
import com.codahale.metrics.Meter;
import com.codahale.metrics.MetricRegistry;
import com.codahale.metrics.Timer;
import com.fasterxml.jackson.annotation.JsonAutoDetect;
import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.fasterxml.jackson.annotation.JsonTypeName;
import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
import com.floreysoft.jmte.Engine;
import com.google.auto.value.AutoValue;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.collect.ArrayListMultimap;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.Multimap;
import com.google.inject.assistedinject.Assisted;
import com.jayway.jsonpath.DocumentContext;
import com.jayway.jsonpath.InvalidJsonException;
import com.jayway.jsonpath.InvalidPathException;
import com.jayway.jsonpath.JsonPath;
import com.jayway.jsonpath.PathNotFoundException;
import jakarta.inject.Inject;
import jakarta.validation.constraints.NotEmpty;
import jakarta.ws.rs.core.HttpHeaders;
import jakarta.ws.rs.core.MediaType;
import okhttp3.Headers;
import okhttp3.HttpUrl;
import okhttp3.OkHttpClient;
import okhttp3.Request;
import okhttp3.Response;
import org.graylog.autovalue.WithBeanGetter;
import org.graylog2.lookup.dto.DataAdapterDto;
import org.graylog2.notifications.Notification;
import org.graylog2.notifications.NotificationService;
import org.graylog2.plugin.lookup.LookupCachePurge;
import org.graylog2.plugin.lookup.LookupDataAdapter;
import org.graylog2.plugin.lookup.LookupDataAdapterConfiguration;
import org.graylog2.plugin.lookup.LookupResult;
import org.graylog2.system.urlwhitelist.UrlNotWhitelistedException;
import org.graylog2.system.urlwhitelist.UrlWhitelistNotificationService;
import org.graylog2.system.urlwhitelist.UrlWhitelistService;
import org.joda.time.Duration;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.annotation.Nullable;
import java.io.IOException;
import java.io.InputStream;
import java.io.UnsupportedEncodingException;
import java.net.URLEncoder;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.stream.Collectors;
import static com.google.common.base.Strings.isNullOrEmpty;
import static org.graylog2.shared.utilities.StringUtils.f;
public class HTTPJSONPathDataAdapter extends LookupDataAdapter {
private static final Logger LOG = LoggerFactory.getLogger(HTTPJSONPathDataAdapter.class);
public static final String NAME = "httpjsonpath";
private final Config config;
private final Engine templateEngine;
private final OkHttpClient httpClient;
private final UrlWhitelistService urlWhitelistService;
private final UrlWhitelistNotificationService urlWhitelistNotificationService;
private final NotificationService notificationService;
private final Timer httpRequestTimer;
private final Meter httpRequestErrors;
private final Meter httpURLErrors;
private JsonPath singleJsonPath = null;
private JsonPath multiJsonPath = null;
private Headers headers;
@Inject
protected HTTPJSONPathDataAdapter(@Assisted("dto") DataAdapterDto dto, Engine templateEngine, OkHttpClient httpClient, UrlWhitelistService urlWhitelistService,
UrlWhitelistNotificationService urlWhitelistNotificationService, MetricRegistry metricRegistry,
NotificationService notificationService) {
super(dto, metricRegistry);
this.config = (Config) dto.config();
this.templateEngine = templateEngine;
// TODO Add config options: caching, timeouts, custom headers, basic auth (See: https://github.com/square/okhttp/wiki/Recipes)
this.httpClient = httpClient.newBuilder().build(); // Copy HTTP client to be able to modify it
this.urlWhitelistService = urlWhitelistService;
this.urlWhitelistNotificationService = urlWhitelistNotificationService;
this.notificationService = notificationService;
this.httpRequestTimer = metricRegistry.timer(MetricRegistry.name(getClass(), "httpRequestTime"));
this.httpRequestErrors = metricRegistry.meter(MetricRegistry.name(getClass(), "httpRequestErrors"));
this.httpURLErrors = metricRegistry.meter(MetricRegistry.name(getClass(), "httpURLErrors"));
}
@Override
protected void doStart() throws Exception {
if (isNullOrEmpty(config.url())) {
throw new IllegalArgumentException("URL needs to be set");
}
if (isNullOrEmpty(config.singleValueJSONPath())) {
throw new IllegalArgumentException("Value JSONPath needs to be set");
}
this.singleJsonPath = JsonPath.compile(config.singleValueJSONPath());
// The JSONPath for the single value cannot be indefinite. (https://github.com/json-path/JsonPath#what-is-returned-when)
if (!singleJsonPath.isDefinite()) {
throw new IllegalArgumentException("Single JSONPath <" + config.singleValueJSONPath() + "> cannot return a list");
}
if (config.multiValueJSONPath().isPresent() && !isNullOrEmpty(config.multiValueJSONPath().get())) {
this.multiJsonPath = JsonPath.compile(config.multiValueJSONPath().get());
}
final Headers.Builder headersBuilder = new Headers.Builder()
.add(HttpHeaders.USER_AGENT, config.userAgent())
.add(HttpHeaders.ACCEPT, MediaType.APPLICATION_JSON);
if (config.headers() != null) {
config.headers().forEach(headersBuilder::set);
}
this.headers = headersBuilder.build();
}
@Override
protected void doStop() throws Exception {
}
@Override
public Duration refreshInterval() {
return Duration.ZERO;
}
@Override
protected void doRefresh(LookupCachePurge cachePurge) throws Exception {
}
@Override
protected LookupResult doGet(Object key) {
String encodedKey;
try {
encodedKey = URLEncoder.encode(String.valueOf(key), "UTF-8").replaceAll("\\+", "%20");
} catch (UnsupportedEncodingException ignored) {
// UTF-8 is always supported
encodedKey = String.valueOf(key);
}
final String urlString = templateEngine.transform(config.url(), ImmutableMap.of("key", encodedKey));
if (!urlWhitelistService.isWhitelisted(urlString)) {
LOG.error("Data adapter <{}>: URL <{}> is not whitelisted. Aborting lookup request.", name(), urlString);
publishSystemNotificationForWhitelistFailure();
setError(UrlNotWhitelistedException.forUrl(urlString));
return getErrorResult();
} else {
// we use this kind of error reporting mechanism only for whitelist errors, so we can safely clear the
// error here
clearError();
}
final HttpUrl url = HttpUrl.parse(urlString);
if (url == null) {
LOG.error("Data adapter <{}>: Couldn't parse URL <{}> - returning empty result", name(), urlString);
httpURLErrors.mark();
return getErrorResult();
}
final Request request = new Request.Builder()
.get()
.url(url)
.headers(headers)
.build();
final Timer.Context time = httpRequestTimer.time();
try (final Response response = httpClient.newCall(request).execute()) {
if (!response.isSuccessful()) {
LOG.warn("HTTP request for key <{}> failed: {}", key, response);
httpRequestErrors.mark();
return getErrorResult();
}
final LookupResult result = parseBody(singleJsonPath, multiJsonPath, response.body().byteStream());
if (result == null) {
return getErrorResult();
}
return result;
} catch (IOException e) {
LOG.error("Data adapter <{}>: HTTP request error for key <{}> from URL <{}>", name(), key, urlString, e);
httpRequestErrors.mark();
Notification systemNotification = notificationService.buildNow()
.addType(Notification.Type.GENERIC)
.addSeverity(Notification.Severity.NORMAL)
.addDetail("title", "HTTP data adapter lookup failure")
.addDetail("description", f("Data adapter <%s>: HTTP request error from URL <%s>: %s", name(), urlString, e.getMessage()));
notificationService.publishIfFirst(systemNotification);
return getErrorResult();
} finally {
time.stop();
}
}
@VisibleForTesting
static LookupResult parseBody(JsonPath singleJsonPath, @Nullable JsonPath multiJsonPath, InputStream body) {
try {
final DocumentContext documentContext = JsonPath.parse(body);
LookupResult.Builder builder = LookupResult.builder().cacheTTL(Long.MAX_VALUE);
if (multiJsonPath != null) {
try {
final Object multiValue = documentContext.read(multiJsonPath);
if (multiValue instanceof Map) {
//noinspection unchecked
builder = builder.multiValue((Map
© 2015 - 2024 Weber Informatics LLC | Privacy Policy