blob: efe334ae2747605669dc6b1a68d8c993acb7b4bf [file] [log] [blame]
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.apache.nifi.minifi.c2.service;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.google.common.base.Supplier;
import com.google.common.base.Suppliers;
import com.google.common.cache.CacheBuilder;
import com.google.common.cache.CacheLoader;
import com.google.common.cache.LoadingCache;
import com.google.common.util.concurrent.UncheckedExecutionException;
import com.wordnik.swagger.annotations.Api;
import org.apache.nifi.minifi.c2.api.Configuration;
import org.apache.nifi.minifi.c2.api.ConfigurationProvider;
import org.apache.nifi.minifi.c2.api.ConfigurationProviderException;
import org.apache.nifi.minifi.c2.api.InvalidParameterException;
import org.apache.nifi.minifi.c2.api.security.authorization.AuthorizationException;
import org.apache.nifi.minifi.c2.api.security.authorization.Authorizer;
import org.apache.nifi.minifi.c2.api.util.Pair;
import org.apache.nifi.minifi.c2.util.HttpRequestUtil;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.security.core.context.SecurityContextHolder;
import javax.servlet.http.HttpServletRequest;
import javax.ws.rs.GET;
import javax.ws.rs.Path;
import javax.ws.rs.Produces;
import javax.ws.rs.WebApplicationException;
import javax.ws.rs.core.Context;
import javax.ws.rs.core.HttpHeaders;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.Response;
import javax.ws.rs.core.UriInfo;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors;
@Path("/config")
@Api(
value = "/config",
description = "Provides configuration for MiNiFi instances"
)
public class ConfigService {
private static final Logger logger = LoggerFactory.getLogger(ConfigService.class);
private final Authorizer authorizer;
private final ObjectMapper objectMapper;
private final Supplier<ConfigurationProviderInfo> configurationProviderInfo;
private final LoadingCache<ConfigurationProviderKey, ConfigurationProviderValue> configurationCache;
public ConfigService(List<ConfigurationProvider> configurationProviders, Authorizer authorizer) {
this(configurationProviders, authorizer, 1000, 300_000);
}
public ConfigService(List<ConfigurationProvider> configurationProviders, Authorizer authorizer, long maximumCacheSize, long cacheTtlMillis) {
this.authorizer = authorizer;
this.objectMapper = new ObjectMapper();
if (configurationProviders == null || configurationProviders.size() == 0) {
throw new IllegalArgumentException("Expected at least one configuration provider");
}
this.configurationProviderInfo = Suppliers.memoizeWithExpiration(() -> initContentTypeInfo(configurationProviders), cacheTtlMillis, TimeUnit.MILLISECONDS);
CacheBuilder<Object, Object> cacheBuilder = CacheBuilder.newBuilder();
if (maximumCacheSize >= 0) {
cacheBuilder = cacheBuilder.maximumSize(maximumCacheSize);
}
if (cacheTtlMillis >= 0) {
cacheBuilder = cacheBuilder.refreshAfterWrite(cacheTtlMillis, TimeUnit.MILLISECONDS);
}
this.configurationCache = cacheBuilder
.build(new CacheLoader<ConfigurationProviderKey, ConfigurationProviderValue>() {
@Override
public ConfigurationProviderValue load(ConfigurationProviderKey key) throws Exception {
return initConfigurationProviderValue(key);
}
});
}
public ConfigurationProviderValue initConfigurationProviderValue(ConfigurationProviderKey key) {
if (logger.isDebugEnabled()) {
logger.debug("Attempting to load and cache configuration with key " + key);
}
try {
List<MediaType> acceptValues = key.getAcceptValues();
Pair<MediaType, ConfigurationProvider> providerPair = getProvider(acceptValues);
Map<String, List<String>> parameters = key.getParameters();
Integer version = null;
List<String> versionList = parameters.get("version");
if (versionList != null && versionList.size() > 0) {
try {
version = Integer.parseInt(versionList.get(0));
} catch (NumberFormatException e) {
throw new InvalidParameterException("Unable to parse " + version + " as integer.", e);
}
}
return new ConfigurationProviderValue(providerPair.getSecond().getConfiguration(providerPair.getFirst().toString(), version, parameters), providerPair.getFirst(), null);
} catch (ConfigurationProviderException e) {
return new ConfigurationProviderValue(null, null, e);
}
}
protected ConfigurationProviderInfo initContentTypeInfo(List<ConfigurationProvider> configurationProviders) {
List<Pair<MediaType, ConfigurationProvider>> mediaTypeList = new ArrayList<>();
List<String> contentTypes = new ArrayList<>();
Set<MediaType> seenMediaTypes = new LinkedHashSet<>();
for (ConfigurationProvider configurationProvider : configurationProviders) {
try {
for (String contentTypeString : configurationProvider.getContentTypes()) {
MediaType mediaType = MediaType.valueOf(contentTypeString);
if (seenMediaTypes.add(mediaType)) {
contentTypes.add(contentTypeString);
mediaTypeList.add(new Pair<>(mediaType, configurationProvider));
}
}
} catch (ConfigurationProviderException e) {
return new ConfigurationProviderInfo(null, null, e);
}
}
return new ConfigurationProviderInfo(mediaTypeList, contentTypes, null);
}
@GET
@Path("/contentTypes")
@Produces(MediaType.APPLICATION_JSON)
public Response getContentTypes(@Context HttpServletRequest request, @Context UriInfo uriInfo) {
try {
authorizer.authorize(SecurityContextHolder.getContext().getAuthentication(), uriInfo);
} catch (AuthorizationException e) {
logger.warn(HttpRequestUtil.getClientString(request) + " not authorized to access " + uriInfo, e);
return Response.status(403).build();
}
ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
List<String> contentTypes;
try {
contentTypes = configurationProviderInfo.get().getContentTypes();
} catch (ConfigurationProviderException e) {
logger.warn("Unable to initialize content type information.", e);
return Response.status(500).build();
}
try {
objectMapper.writerWithDefaultPrettyPrinter().writeValue(byteArrayOutputStream, contentTypes);
} catch (IOException e) {
logger.warn("Unable to write configuration providers to output stream.", e);
return Response.status(500).build();
}
return Response.ok().type(MediaType.APPLICATION_JSON_TYPE).entity(byteArrayOutputStream.toByteArray()).build();
}
@GET
public Response getConfig(@Context HttpServletRequest request, @Context HttpHeaders httpHeaders, @Context UriInfo uriInfo) {
try {
authorizer.authorize(SecurityContextHolder.getContext().getAuthentication(), uriInfo);
} catch (AuthorizationException e) {
logger.warn(HttpRequestUtil.getClientString(request) + " not authorized to access " + uriInfo, e);
return Response.status(403).build();
}
Map<String, List<String>> parameters = new HashMap<>();
for (Map.Entry<String, List<String>> entry : uriInfo.getQueryParameters().entrySet()) {
parameters.put(entry.getKey(), entry.getValue());
}
List<MediaType> acceptValues = httpHeaders.getAcceptableMediaTypes();
boolean defaultAccept = false;
if (acceptValues.size() == 0) {
acceptValues = Arrays.asList(MediaType.WILDCARD_TYPE);
defaultAccept = true;
}
if (logger.isDebugEnabled()) {
StringBuilder builder = new StringBuilder("Handling request from ")
.append(HttpRequestUtil.getClientString(request))
.append(" with parameters ")
.append(parameters)
.append(" and Accept");
if (defaultAccept) {
builder = builder.append(" default value");
}
builder = builder.append(": ")
.append(acceptValues.stream().map(Object::toString).collect(Collectors.joining(", ")));
logger.debug(builder.toString());
}
try {
ConfigurationProviderValue configurationProviderValue = configurationCache.get(new ConfigurationProviderKey(acceptValues, parameters));
Configuration configuration = configurationProviderValue.getConfiguration();
Response.ResponseBuilder ok = Response.ok();
ok = ok.header("X-Content-Version", configuration.getVersion());
ok = ok.type(configurationProviderValue.getMediaType());
byte[] buffer = new byte[1024];
int read;
try (InputStream inputStream = configuration.getInputStream();
ByteArrayOutputStream outputStream = new ByteArrayOutputStream()) {
MessageDigest md5 = MessageDigest.getInstance("MD5");
MessageDigest sha256 = MessageDigest.getInstance("SHA-256");
while ((read = inputStream.read(buffer)) >= 0) {
outputStream.write(buffer, 0, read);
md5.update(buffer, 0, read);
sha256.update(buffer, 0, read);
}
ok = ok.header("Content-MD5", bytesToHex(md5.digest()));
ok = ok.header("X-Content-SHA-256", bytesToHex(sha256.digest()));
ok = ok.entity(outputStream.toByteArray());
} catch (ConfigurationProviderException | IOException | NoSuchAlgorithmException e) {
logger.error("Error reading or checksumming configuration file", e);
throw new WebApplicationException(500);
}
return ok.build();
} catch (AuthorizationException e) {
logger.warn(HttpRequestUtil.getClientString(request) + " not authorized to access " + uriInfo, e);
return Response.status(403).build();
} catch (InvalidParameterException e) {
logger.info(HttpRequestUtil.getClientString(request) + " made invalid request with " + HttpRequestUtil.getQueryString(request), e);
return Response.status(400).entity("Invalid request.").build();
} catch (ConfigurationProviderException e) {
logger.warn("Unable to get configuration.", e);
return Response.status(500).build();
} catch (ExecutionException|UncheckedExecutionException e) {
Throwable cause = e.getCause();
if (cause instanceof WebApplicationException) {
throw (WebApplicationException) cause;
}
logger.error(HttpRequestUtil.getClientString(request) + " made request with " + HttpRequestUtil.getQueryString(request) + " that caused error.", cause);
return Response.status(500).entity("Internal error").build();
}
}
// see: http://stackoverflow.com/questions/15429257/how-to-convert-byte-array-to-hexstring-in-java#answer-15429408
protected static String bytesToHex(byte[] in) {
final StringBuilder builder = new StringBuilder();
for (byte b : in) {
builder.append(String.format("%02x", b));
}
return builder.toString();
}
private Pair<MediaType, ConfigurationProvider> getProvider(List<MediaType> acceptValues) throws ConfigurationProviderException {
List<Pair<MediaType, ConfigurationProvider>> mediaTypeList;
try {
mediaTypeList = this.configurationProviderInfo.get().getMediaTypeList();
} catch (ConfigurationProviderException.Wrapper e) {
throw e.unwrap();
}
for (MediaType accept : acceptValues) {
for (Pair<MediaType, ConfigurationProvider> pair : mediaTypeList) {
MediaType mediaType = pair.getFirst();
if (accept.isCompatible(mediaType)) {
return new Pair<>(mediaType, pair.getSecond());
}
}
}
throw new WebApplicationException(Response.status(406).entity("Unable to find configuration provider for " +
"\"Accept: " + acceptValues.stream().map(Object::toString).collect(Collectors.joining(", ")) + "\" supported media types are " +
mediaTypeList.stream().map(Pair::getFirst).map(Object::toString).collect(Collectors.joining(", "))).build());
}
}