org.swisspush.gateleen.hook.HookHandler Maven / Gradle / Ivy
package org.swisspush.gateleen.hook;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.networknt.schema.JsonSchema;
import com.networknt.schema.JsonSchemaFactory;
import com.networknt.schema.ValidationMessage;
import io.vertx.core.AsyncResult;
import io.vertx.core.Handler;
import io.vertx.core.Vertx;
import io.vertx.core.buffer.Buffer;
import io.vertx.core.eventbus.Message;
import io.vertx.core.http.HttpClient;
import io.vertx.core.http.HttpClientRequest;
import io.vertx.core.http.HttpClientResponse;
import io.vertx.core.http.HttpMethod;
import io.vertx.core.http.HttpServerRequest;
import io.vertx.core.http.HttpServerResponse;
import io.vertx.core.http.impl.headers.HeadersMultiMap;
import io.vertx.core.json.DecodeException;
import io.vertx.core.json.JsonArray;
import io.vertx.core.json.JsonObject;
import io.vertx.core.net.ProxyOptions;
import io.vertx.ext.web.RoutingContext;
import org.joda.time.DateTime;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.swisspush.gateleen.core.http.HeaderFunction;
import org.swisspush.gateleen.core.http.HeaderFunctions;
import org.swisspush.gateleen.core.http.HttpRequest;
import org.swisspush.gateleen.core.logging.LoggableResource;
import org.swisspush.gateleen.core.logging.RequestLogger;
import org.swisspush.gateleen.core.storage.ResourceStorage;
import org.swisspush.gateleen.core.util.CollectionContentComparator;
import org.swisspush.gateleen.core.util.HttpHeaderUtil;
import org.swisspush.gateleen.core.util.HttpRequestHeader;
import org.swisspush.gateleen.core.util.HttpServerRequestUtil;
import org.swisspush.gateleen.core.util.ResourcesUtils;
import org.swisspush.gateleen.core.util.StatusCode;
import org.swisspush.gateleen.hook.queueingstrategy.DefaultQueueingStrategy;
import org.swisspush.gateleen.hook.queueingstrategy.DiscardPayloadQueueingStrategy;
import org.swisspush.gateleen.hook.queueingstrategy.QueueingStrategy;
import org.swisspush.gateleen.hook.queueingstrategy.QueueingStrategyFactory;
import org.swisspush.gateleen.hook.queueingstrategy.ReducedPropagationQueueingStrategy;
import org.swisspush.gateleen.hook.reducedpropagation.ReducedPropagationManager;
import org.swisspush.gateleen.logging.LogAppenderRepository;
import org.swisspush.gateleen.logging.LoggingResourceManager;
import org.swisspush.gateleen.monitoring.MonitoringHandler;
import org.swisspush.gateleen.queue.expiry.ExpiryCheckHandler;
import org.swisspush.gateleen.queue.queuing.QueueClient;
import org.swisspush.gateleen.queue.queuing.QueueProcessor;
import org.swisspush.gateleen.queue.queuing.RequestQueue;
import org.swisspush.gateleen.queue.queuing.splitter.NoOpQueueSplitter;
import org.swisspush.gateleen.queue.queuing.splitter.QueueSplitter;
import org.swisspush.gateleen.routing.Router;
import org.swisspush.gateleen.routing.Rule;
import org.swisspush.gateleen.routing.RuleFactory;
import org.swisspush.gateleen.validation.RegexpValidator;
import org.swisspush.gateleen.validation.ValidationException;
import javax.annotation.Nonnull;
import javax.annotation.Nullable;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.Set;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.function.Consumer;
import java.util.regex.Pattern;
import java.util.stream.Collectors;
import static io.vertx.core.http.HttpMethod.DELETE;
import static io.vertx.core.http.HttpMethod.PUT;
import static org.swisspush.gateleen.core.util.HttpRequestHeader.CONTENT_LENGTH;
/**
* The HookHandler is responsible for un- and registering hooks (listener, as well as routes). He also
* handles forwarding requests to listeners / routes.
*
* @author https://github.com/ljucam [Mario Ljuca]
*/
public class HookHandler implements LoggableResource {
public static final String HOOKED_HEADER = "x-hooked";
public static final String HOOK_ROUTES_LISTED = "x-hook-routes-listed";
public static final String HOOKS_LISTENERS_URI_PART = "/_hooks/listeners/";
public static final String LISTENER_QUEUE_PREFIX = "listener-hook";
private static final String X_QUEUE = "x-queue";
private static final String X_EXPIRE_AFTER = "X-Expire-After";
private static final String LISTENER_HOOK_TARGET_PATH = "listeners/";
public static final String HOOKS_ROUTE_URI_PART = "/_hooks/route";
private static final String HOOK_STORAGE_PATH = "registrations/";
private static final String HOOK_LISTENER_STORAGE_PATH = HOOK_STORAGE_PATH + "listeners/";
private static final String HOOK_ROUTE_STORAGE_PATH = HOOK_STORAGE_PATH + "routes/";
private static final String SAVE_LISTENER_ADDRESS = "gateleen.hook-listener-insert";
private static final String REMOVE_LISTENER_ADDRESS = "gateleen.hook-listener-remove";
private static final String SAVE_ROUTE_ADDRESS = "gateleen.hook-route-insert";
private static final String REMOVE_ROUTE_ADDRESS = "gateleen.hook-route-remove";
private static final int STATUS_CODE_2XX = 2;
private static final int DEFAULT_HOOK_STORAGE_EXPIRE_AFTER_TIME = 60 * 60; // 1h in seconds
private static final int DEFAULT_CLEANUP_TIME = 15000; // 15 seconds
public static final String REQUESTURL = "requesturl";
public static final String EXPIRATION_TIME = "expirationTime";
public static final String HOOK = "hook";
public static final String TRANSLATE_STATUS = "translateStatus";
public static final String METHODS = "methods";
public static final String HEADERS_FILTER = "headersFilter";
public static final String DESTINATION = "destination";
public static final String FILTER = "filter";
public static final String QUEUE_EXPIRE_AFTER = "queueExpireAfter";
public static final String STATIC_HEADERS = "staticHeaders";
public static final String FULL_URL = "fullUrl";
public static final String DISCARD_PAYLOAD = "discardPayload";
public static final String HOOK_TRIGGER_TYPE = "type";
public static final String LISTABLE = "listable";
public static final String COLLECTION = "collection";
private final Comparator collectionContentComparator;
private static final Logger log = LoggerFactory.getLogger(HookHandler.class);
private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper();
private final Vertx vertx;
private final ResourceStorage userProfileStorage;
private final ResourceStorage hookStorage;
private final MonitoringHandler monitoringHandler;
private final LoggingResourceManager loggingResourceManager;
private final LogAppenderRepository logAppenderRepository;
private final HttpClient selfClient;
private final String userProfilePath;
private final String hookRootUri;
private final boolean listableRoutes;
private final ListenerRepository listenerRepository;
final RouteRepository routeRepository;
private final RequestQueue requestQueue;
private final ReducedPropagationManager reducedPropagationManager;
private boolean logHookConfigurationResourceChanges = false;
private final Handler doneHandler;
private final JsonSchema jsonSchemaHook;
private int routeMultiplier;
private final QueueSplitter queueSplitter;
/**
* Creates a new HookHandler.
*
* @param vertx vertx
* @param selfClient selfClient
* @param storage storage
* @param loggingResourceManager loggingResourceManager
* @param monitoringHandler monitoringHandler
* @param userProfilePath userProfilePath
* @param hookRootUri hookRootUri
*/
public HookHandler(Vertx vertx, HttpClient selfClient, final ResourceStorage storage,
LoggingResourceManager loggingResourceManager, LogAppenderRepository logAppenderRepository, MonitoringHandler monitoringHandler,
String userProfilePath, String hookRootUri) {
this(vertx, selfClient, storage, loggingResourceManager, logAppenderRepository, monitoringHandler, userProfilePath, hookRootUri,
new QueueClient(vertx, monitoringHandler));
}
/**
* Creates a new HookHandler.
*
* @param vertx vertx
* @param selfClient selfClient
* @param storage storage
* @param loggingResourceManager loggingResourceManager
* @param monitoringHandler monitoringHandler
* @param userProfilePath userProfilePath
* @param hookRootUri hookRootUri
* @param requestQueue requestQueue
*/
public HookHandler(Vertx vertx, HttpClient selfClient, final ResourceStorage storage,
LoggingResourceManager loggingResourceManager, LogAppenderRepository logAppenderRepository, MonitoringHandler monitoringHandler,
String userProfilePath, String hookRootUri, RequestQueue requestQueue) {
this(vertx, selfClient, storage, loggingResourceManager, logAppenderRepository, monitoringHandler, userProfilePath, hookRootUri,
requestQueue, false);
}
public HookHandler(Vertx vertx, HttpClient selfClient, final ResourceStorage storage,
LoggingResourceManager loggingResourceManager, LogAppenderRepository logAppenderRepository, MonitoringHandler monitoringHandler,
String userProfilePath, String hookRootUri, RequestQueue requestQueue, boolean listableRoutes) {
this(vertx, selfClient, storage, loggingResourceManager, logAppenderRepository, monitoringHandler, userProfilePath, hookRootUri,
requestQueue, false, null);
}
/**
* Creates a new HookHandler.
*
* @param vertx vertx
* @param selfClient selfClient
* @param storage storage
* @param loggingResourceManager loggingResourceManager
* @param monitoringHandler monitoringHandler
* @param userProfilePath userProfilePath
* @param hookRootUri hookRootUri
* @param requestQueue requestQueue
* @param listableRoutes listableRoutes
* @param reducedPropagationManager reducedPropagationManager
*/
public HookHandler(Vertx vertx, HttpClient selfClient, final ResourceStorage storage,
LoggingResourceManager loggingResourceManager, LogAppenderRepository logAppenderRepository, MonitoringHandler monitoringHandler,
String userProfilePath, String hookRootUri, RequestQueue requestQueue, boolean listableRoutes,
@Nullable ReducedPropagationManager reducedPropagationManager) {
this(vertx, selfClient, storage, loggingResourceManager, logAppenderRepository, monitoringHandler, userProfilePath, hookRootUri,
requestQueue, listableRoutes, reducedPropagationManager, null, storage);
}
public HookHandler(Vertx vertx, HttpClient selfClient, final ResourceStorage userProfileStorage,
LoggingResourceManager loggingResourceManager, LogAppenderRepository logAppenderRepository, MonitoringHandler monitoringHandler,
String userProfilePath, String hookRootUri, RequestQueue requestQueue, boolean listableRoutes,
ReducedPropagationManager reducedPropagationManager, @Nullable Handler doneHandler, ResourceStorage hookStorage) {
this(vertx, selfClient, userProfileStorage, loggingResourceManager, logAppenderRepository, monitoringHandler, userProfilePath, hookRootUri,
requestQueue, listableRoutes, reducedPropagationManager, doneHandler, hookStorage, Router.DEFAULT_ROUTER_MULTIPLIER);
}
public HookHandler(Vertx vertx, HttpClient selfClient, final ResourceStorage userProfileStorage,
LoggingResourceManager loggingResourceManager, LogAppenderRepository logAppenderRepository, MonitoringHandler monitoringHandler,
String userProfilePath, String hookRootUri, RequestQueue requestQueue, boolean listableRoutes,
ReducedPropagationManager reducedPropagationManager, @Nullable Handler doneHandler, ResourceStorage hookStorage,
int routeMultiplier) {
this(vertx, selfClient, userProfileStorage, loggingResourceManager, logAppenderRepository, monitoringHandler, userProfilePath, hookRootUri,
requestQueue, listableRoutes, reducedPropagationManager, doneHandler, hookStorage, routeMultiplier, new NoOpQueueSplitter());
}
/**
* Creates a new HookHandler.
*
* @param vertx vertx
* @param selfClient selfClient
* @param userProfileStorage userProfileStorage - where the user profiles are stored
* @param loggingResourceManager loggingResourceManager
* @param monitoringHandler monitoringHandler
* @param userProfilePath userProfilePath
* @param hookRootUri hookRootUri
* @param requestQueue requestQueue
* @param listableRoutes listableRoutes
* @param reducedPropagationManager reducedPropagationManager
* @param doneHandler doneHandler
* @param hookStorage hookStorage - where the hooks are stored
* @param routeMultiplier the multiplier that is applied to routes, this is typically the number of nodes in
* a cluster multiplied by the number of router instances within a node. Or in other words
* the number of {@link Router} instances within a cluster
* @param queueSplitter Configured QueueSplitter or NoOpQueueSplitter which dispatches to subqueues for
* parallel operation.
*/
public HookHandler(Vertx vertx, HttpClient selfClient, final ResourceStorage userProfileStorage,
LoggingResourceManager loggingResourceManager, LogAppenderRepository logAppenderRepository, MonitoringHandler monitoringHandler,
String userProfilePath, String hookRootUri, RequestQueue requestQueue, boolean listableRoutes,
ReducedPropagationManager reducedPropagationManager, @Nullable Handler doneHandler, ResourceStorage hookStorage,
int routeMultiplier, @Nonnull QueueSplitter queueSplitter) {
log.debug("Creating HookHandler ...");
this.vertx = vertx;
this.selfClient = selfClient;
this.userProfileStorage = userProfileStorage;
this.loggingResourceManager = loggingResourceManager;
this.logAppenderRepository = logAppenderRepository;
this.monitoringHandler = monitoringHandler;
this.userProfilePath = userProfilePath;
this.hookRootUri = hookRootUri;
this.requestQueue = requestQueue;
this.listableRoutes = listableRoutes;
this.reducedPropagationManager = reducedPropagationManager;
listenerRepository = new LocalListenerRepository();
routeRepository = new LocalRouteRepository();
collectionContentComparator = new CollectionContentComparator();
this.doneHandler = doneHandler;
this.hookStorage = hookStorage;
this.routeMultiplier = routeMultiplier;
this.queueSplitter = queueSplitter;
String hookSchema = ResourcesUtils.loadResource("gateleen_hooking_schema_hook", true);
jsonSchemaHook = JsonSchemaFactory.getInstance().getSchema(hookSchema);
}
public void init() {
// add all init methods here (!)
final List>> initMethods = new ArrayList<>();
initMethods.add(this::registerListenerRegistrationHandler);
initMethods.add(this::registerRouteRegistrationHandler);
initMethods.add(this::loadStoredListeners);
initMethods.add(this::loadStoredRoutes);
initMethods.add(this::registerCleanupHandler);
initMethods.add(this::registerRouteMultiplierChangeHandler);
// ready handler, calls the doneHandler when everything is done and the HookHandler is ready to use
Handler readyHandler = new Handler<>() {
// count of methods with may return an OK (ready)
private final AtomicInteger readyCounter = new AtomicInteger(initMethods.size());
@Override
public void handle(Void aVoid) {
if (readyCounter.decrementAndGet() == 0) {
log.info("HookHandler is ready!");
if (doneHandler != null) {
doneHandler.handle(null);
}
}
}
};
initMethods.forEach(handlerConsumer -> handlerConsumer.accept(readyHandler));
}
@Override
public void enableResourceLogging(boolean resourceLoggingEnabled) {
this.logHookConfigurationResourceChanges = resourceLoggingEnabled;
}
/**
* Registers a cleanup timer
*
* @param readyHandler - the ready handler
*/
private void registerCleanupHandler(Handler readyHandler) {
vertx.setPeriodic(DEFAULT_CLEANUP_TIME, timerID -> {
log.trace("Running hook cleanup ...");
DateTime now = DateTime.now();
// Loop through listeners first
for (Listener listener : listenerRepository.getListeners()) {
final Optional expirationTime = listener.getHook().getExpirationTime();
if (expirationTime.isEmpty()) {
if (log.isTraceEnabled()) {
log.trace("Listener {} will never expire.", listener.getListenerId());
}
} else if (expirationTime.get().isBefore(now)) {
log.debug("Listener {} expired at {} and current time is {}", listener.getListenerId(), expirationTime.get(), now);
listenerRepository.removeListener(listener.getListenerId());
routeRepository.removeRoute(hookRootUri + LISTENER_HOOK_TARGET_PATH + listener.getListenerId());
}
}
// Loop through routes
Map routes = routeRepository.getRoutes();
for (String key : routes.keySet()) {
Route route = routes.get(key);
final Optional expirationTime = route.getHook().getExpirationTime();
if (expirationTime.isEmpty()) {
if (log.isTraceEnabled()) {
log.trace("Route {} will never expire.", key);
}
} else if (expirationTime.get().isBefore(now)) {
routeRepository.removeRoute(key);
}
}
monitoringHandler.updateListenerCount(listenerRepository.size());
monitoringHandler.updateRoutesCount(routeRepository.getRoutes().size());
log.trace("done");
});
// method done / no async processing pending
readyHandler.handle(null);
}
/**
* Loads the stored routes
* from the resource hookStorage,
* if any are available.
*
* @param readyHandler - the ready handler
*/
private void loadStoredRoutes(Handler readyHandler) {
log.debug("loadStoredRoutes");
// load the names of the routes from the hookStorage
final String routeBase = hookRootUri + HOOK_ROUTE_STORAGE_PATH;
hookStorage.get(routeBase, buffer -> {
if (buffer != null) {
JsonObject listOfRoutes = new JsonObject(buffer.toString());
JsonArray routeNames = listOfRoutes.getJsonArray("routes");
Iterator keys = routeNames.getList().iterator();
final AtomicInteger storedRoutesCount = new AtomicInteger(routeNames.getList().size());
// go through the routes ...
while (keys.hasNext()) {
final String key = keys.next();
// ... and load each one
hookStorage.get(routeBase + key, routeBody -> {
if (routeBody != null) {
registerRoute(routeBody);
} else {
log.warn("Could not get URL '{}' (getting hook route).", routeBase + key);
}
// send a ready flag
if (storedRoutesCount.decrementAndGet() == 0) {
readyHandler.handle(null);
}
});
}
} else {
log.warn("Could not get URL '{}' (getting hook route).", routeBase);
// send a ready flag
readyHandler.handle(null);
}
});
}
/**
* Loads the stored listeners
* from the resource hookStorage, if
* any are available.
*
* @param readyHandler - the ready handler
*/
private void loadStoredListeners(final Handler readyHandler) {
log.debug("loadStoredListeners");
// load the names of the listener from the hookStorage
final String listenerBase = hookRootUri + HOOK_LISTENER_STORAGE_PATH;
hookStorage.get(listenerBase, buffer -> {
if (buffer != null) {
JsonObject listOfListeners = new JsonObject(buffer.toString());
JsonArray listenerNames = listOfListeners.getJsonArray("listeners");
Iterator keys = listenerNames.getList().iterator();
final AtomicInteger storedListenerCount = new AtomicInteger(listenerNames.getList().size());
// go through the listeners ...
while (keys.hasNext()) {
final String key = keys.next();
// ... and load each one
hookStorage.get(listenerBase + key, listenerBody -> {
if (listenerBody != null) {
registerListener(listenerBody);
} else {
log.warn("Could not get URL '{}' (getting hook listener).", listenerBase + key);
}
// send a ready flag
if (storedListenerCount.decrementAndGet() == 0) {
readyHandler.handle(null);
}
});
}
} else {
log.warn("Could not get URL '{}' (getting hook listener).", listenerBase);
// send a ready flag
readyHandler.handle(null);
}
});
}
/**
* Registers all needed handlers for the
* route registration / unregistration.
*
* @param readyHandler - the ready handler
*/
private void registerRouteRegistrationHandler(Handler readyHandler) {
// Receive listener insert notifications
vertx.eventBus().consumer(SAVE_ROUTE_ADDRESS, (Handler>) event -> hookStorage.get(event.body(), buffer -> {
if (buffer != null) {
registerRoute(buffer);
} else {
log.warn("Could not get URL '{}' (getting hook route).", (event.body() == null ? "" : event.body()));
}
}));
// Receive listener remove notifications
vertx.eventBus().consumer(REMOVE_ROUTE_ADDRESS, (Handler>) event -> unregisterRoute(event.body()));
// method done / no async processing pending
readyHandler.handle(null);
}
/**
* Update all registered router's pool size multiplier
*
* @param readyHandler - the ready handler
*/
private void registerRouteMultiplierChangeHandler(Handler readyHandler) {
vertx.eventBus().consumer(Router.ROUTE_MULTIPLIER_ADDRESS, (Handler>) event -> {
log.info("Updating route multiplier: {}", (event.body() == null ? "" : event.body()));
try {
routeMultiplier = Integer.parseInt(event.body());
} catch (NumberFormatException e) {
log.info("failed to parse route multiplier: {}", event.body(), e);
}
});
// method done / no async processing pending
readyHandler.handle(null);
}
/**
* Registers all needed handlers for the
* listener registration / unregistration.
*/
public void registerListenerRegistrationHandler(Handler readyHandler) {
// Receive listener insert notifications
vertx.eventBus().consumer(SAVE_LISTENER_ADDRESS, (Handler>) event -> hookStorage.get(event.body(), buffer -> {
if (buffer != null) {
registerListener(buffer);
} else {
log.warn("Could not get URL '{}' (getting hook listener).", (event.body() == null ? "" : event.body()));
}
}));
// Receive listener remove notifications
vertx.eventBus().consumer(REMOVE_LISTENER_ADDRESS, (Handler>) event -> unregisterListener(event.body()));
// method done / no async processing pending
readyHandler.handle(null);
}
/**
* Handles requests, which are either listener or
* route related.
* Takes on:
*
* - hook un-/registration
* - enqueueing a request for the registered listeners
* - forwarding a request to the reistered listeners
* - creating a self request for the original request (if necessary)
*
*
* @param ctx routing context
* @return true if a request is processed by the handler, otherwise false
*/
public boolean handle(final RoutingContext ctx) {
HttpServerRequest request = ctx.request();
boolean consumed = false;
/*
* 1) Un- / Register Listener / Routes
*/
var requestMethod = request.method();
if (requestMethod == PUT) {
var requestUri = request.uri();
if (requestUri.contains(HOOKS_LISTENERS_URI_PART)) {
handleListenerRegistration(request);
return true;
}
if (requestUri.contains(HOOKS_ROUTE_URI_PART)) {
handleRouteRegistration(request);
return true;
}
}
if (requestMethod == DELETE) {
var requestUri = request.uri();
if (requestUri.contains(HOOKS_LISTENERS_URI_PART)) {
handleListenerUnregistration(request);
return true;
}
if (requestUri.contains(HOOKS_ROUTE_URI_PART)) {
handleRouteUnregistration(request);
return true;
}
}
/*
* 2) Check if we have to queue a request for listeners
*/
final List listeners = listenerRepository.findListeners(request.uri(), request.method().name(), request.headers());
if (!listeners.isEmpty() && !isRequestAlreadyHooked(request)) {
installBodyHandler(ctx, listeners);
consumed = true;
}
if (!consumed) {
consumed = routeRequestIfNeeded(ctx);
if (!consumed) {
return createListingIfRequested(request);
}
return consumed;
} else {
return true;
}
}
/**
* Create a listing of routes in the given parent. This happens
* only if we have a GET request, the routes are listable and
* the request is not marked as already listed (x-hook-routes-listed:true).
*
* @param request request
* @return true if a listing was performed (consumed), otherwise false.
*/
private boolean createListingIfRequested(final HttpServerRequest request) {
String routesListedHeader = request.headers().get(HOOK_ROUTES_LISTED);
boolean routesListed = routesListedHeader != null && routesListedHeader.equals("true");
// GET request / routes not yet listed
if (request.method().equals(HttpMethod.GET) && !routesListed) {
// route collection available for parent?
final List collections = new ArrayList<>(routeRepository.getCollections(request.uri()));
if (!collections.isEmpty()) {
String parentUri = request.uri().contains("?") ? request.uri().substring(0, request.uri().indexOf('?')) : request.uri();
final String parentCollection = getCollectionName(parentUri);
// sort the result array
collections.sort(collectionContentComparator);
if (log.isTraceEnabled()) {
log.trace("createListingIfRequested > (parentUri) {}, (parentCollection) {}", parentUri, parentCollection);
}
selfClient.request(request.method(), request.uri()).onComplete(asyncReqResult -> {
if (asyncReqResult.failed()) {
log.warn("Failed request to {}: {}", request.uri(), asyncReqResult.cause());
return;
}
HttpClientRequest selfRequest = asyncReqResult.result();
if (request.headers() != null && !request.headers().isEmpty()) {
selfRequest.headers().setAll(request.headers());
}
// mark request as already listed
selfRequest.headers().add(HOOK_ROUTES_LISTED, "true");
selfRequest.exceptionHandler(exception -> log.warn("HookHandler: listing of collections (routes) failed: {}: {}", request.uri(), exception.getMessage()));
selfRequest.idleTimeout(120000); // avoids blocking other requests
selfRequest.send(asyncResult -> {
HttpClientResponse response = asyncResult.result();
HttpServerRequestUtil.prepareResponse(request, response);
request.response().headers().remove(HOOK_ROUTES_LISTED);
// if everything is fine, we add the listed collections to the given array
if (response.statusCode() == StatusCode.OK.getStatusCode()) {
if (log.isTraceEnabled()) {
log.trace("createListingIfRequested > use existing array");
}
response.handler(data -> {
JsonObject responseObject = new JsonObject(data.toString());
// we only got an array back, if we perform a simple request
if (responseObject.getValue(parentCollection) instanceof JsonArray) {
JsonArray parentCollectionArray = responseObject.getJsonArray(parentCollection);
// add the listed routes
collections.forEach(parentCollectionArray::add);
}
if (log.isTraceEnabled()) {
log.trace("createListingIfRequested > response: {}", responseObject);
}
// write the response
request.response().write(Buffer.buffer(responseObject.toString()));
});
}
// if nothing is found, we create a new array
else if (response.statusCode() == StatusCode.NOT_FOUND.getStatusCode()) {
if (log.isTraceEnabled()) {
log.trace("createListingIfRequested > creating new array");
}
response.handler(data -> {
// override status message and code
request.response().setStatusCode(StatusCode.OK.getStatusCode());
request.response().setStatusMessage(StatusCode.OK.getStatusMessage());
JsonObject responseObject = new JsonObject();
JsonArray parentCollectionArray = new JsonArray();
responseObject.put(parentCollection, parentCollectionArray);
// add the listed routes
collections.forEach(parentCollectionArray::add);
if (log.isTraceEnabled()) {
log.trace("createListingIfRequested > response: {}", responseObject);
}
// write the response
request.response().write(Buffer.buffer(responseObject.toString()));
});
}
// something's wrong ...
else {
log.debug("createListingIfRequested - got response - ERROR");
response.handler(data -> request.response().write(data));
}
response.endHandler(v -> request.response().end());
});
});
// consumed
return true;
}
}
// not consumed
return false;
}
private String getCollectionName(String url) {
if (url.endsWith("/")) {
url = url.substring(0, url.lastIndexOf("/"));
}
return url.substring(url.lastIndexOf("/") + 1, url.length());
}
private boolean routeRequestIfNeeded(RoutingContext ctx) {
Route route = routeRepository.getRoute(ctx.request().uri());
if (doMethodsMatch(route, ctx) && doHeadersMatch(route, ctx)) {
log.debug("Forward request {}", ctx.request().uri());
route.forward(ctx);
return true;
} else {
return false;
}
}
private boolean doMethodsMatch(Route route, RoutingContext ctx) {
return route != null &&
(route.getHook().getMethods().isEmpty() || route.getHook().getMethods().contains(ctx.request().method().name()));
}
private boolean doHeadersMatch(Route route, RoutingContext ctx) {
if (route == null) {
return false;
}
if (route.getHook().getHeadersFilterPattern() == null) {
return true;
}
Pattern headersFilterPattern = route.getHook().getHeadersFilterPattern();
log.debug("Looking for request headers with pattern {}", headersFilterPattern.pattern());
return HttpHeaderUtil.hasMatchingHeader(ctx.request().headers(), headersFilterPattern);
}
private void installBodyHandler(final RoutingContext ctx, final List listeners) {
// Read the original request and queue a new one for every listener
ctx.request().bodyHandler(buffer -> {
// Create separate lists with filtered listeners
List beforeListener = getFilteredListeners(listeners, HookTriggerType.BEFORE);
List afterListener = getFilteredListeners(listeners, HookTriggerType.AFTER);
// Create handlers for before/after - cases
Handler afterHandler = installAfterHandler(ctx, buffer, afterListener);
Handler beforeHandler = installBeforeHandler(ctx, buffer, beforeListener, afterHandler);
// call the listeners (before)
callListener(ctx, buffer, beforeListener, beforeHandler);
});
}
/**
* Calls the passed listeners and passes the given handler to the enqueued listener requests.
*
* @param ctx original request context
* @param buffer buffer
* @param filteredListeners all listeners which should be called
* @param handler the handler, which should handle the requests
*/
private void callListener(RoutingContext ctx, final Buffer buffer, final List filteredListeners, final Handler handler) {
HttpServerRequest request = ctx.request();
for (Listener listener : filteredListeners) {
log.debug("Enqueue request matching {} {} with listener {}", request.method(), listener.getMonitoredUrl(), listener.getListener());
/*
* url suffix (path) after monitored url
* => monitored url = http://a/b/c
* => request.uri() = http://a/b/c/d/e.x
* => url suffix = /d/e.x
*/
String path = request.uri();
if (!listener.getHook().isFullUrl()) {
path = request.uri().replace(listener.getMonitoredUrl(), "");
}
String targetUri;
// internal
if (listener.getHook().getDestination().startsWith("/")) {
targetUri = listener.getListener() + path;
log.debug(" > internal target: {}", targetUri);
}
// external
else {
targetUri = hookRootUri + LISTENER_HOOK_TARGET_PATH + listener.getListener() + path;
log.debug(" > external target: {}", targetUri);
}
// Create a new multimap, copied from the original request,
// so that the original request is not overridden with the new values.
HeadersMultiMap queueHeaders = new HeadersMultiMap();
queueHeaders.addAll(request.headers());
// Apply the header manipulation chain - errors (unresolvable references) will just be WARN logged - but we still enqueue
final HeaderFunctions.EvalScope evalScope = listener.getHook().getHeaderFunction().apply(queueHeaders);
if (evalScope.getErrorMessage() != null) {
log.warn("problem applying header manipulator chain {} in listener {}", evalScope.getErrorMessage(), listener.getListenerId());
}
if (ExpiryCheckHandler.getQueueExpireAfter(queueHeaders) == null && listener.getHook().getQueueExpireAfter() != -1) {
ExpiryCheckHandler.setQueueExpireAfter(queueHeaders, listener.getHook().getQueueExpireAfter());
}
// if there is an x-queue header (after applying the header manipulator chain!),
// then directly enqueue to this queue - else enqueue to a queue named alike this listener hook
String queue = queueHeaders.get(X_QUEUE);
if (queue == null) {
queue = LISTENER_QUEUE_PREFIX + "-" + listener.getListenerId(); // default queue name for this listener hook
} else {
queueHeaders.remove(X_QUEUE); // remove the "x-queue" header - otherwise we take a second turn through the queue
}
queue = queueSplitter.convertToSubQueue(queue, request);
QueueingStrategy queueingStrategy = listener.getHook().getQueueingStrategy();
if (queueingStrategy instanceof DefaultQueueingStrategy) {
requestQueue.enqueue(new HttpRequest(request.method(), targetUri, queueHeaders, buffer.getBytes()), queue, handler);
} else if (queueingStrategy instanceof DiscardPayloadQueueingStrategy) {
if (HttpRequestHeader.containsHeader(queueHeaders, CONTENT_LENGTH)) {
queueHeaders.set(CONTENT_LENGTH.getName(), "0");
}
requestQueue.enqueue(new HttpRequest(request.method(), targetUri, queueHeaders, null), queue, handler);
} else if (queueingStrategy instanceof ReducedPropagationQueueingStrategy) {
if (reducedPropagationManager != null) {
reducedPropagationManager.processIncomingRequest(request.method(), targetUri, queueHeaders, buffer,
queue, ((ReducedPropagationQueueingStrategy) queueingStrategy).getPropagationIntervalMs(), handler);
} else {
log.error("ReducedPropagationQueueingStrategy without configured ReducedPropagationManager. " +
"Not going to handle (enqueue) anything!");
}
} else {
log.error("QueueingStrategy '{}' is not handled. Could be an error, check the source code!",
queueingStrategy.getClass().getSimpleName());
}
}
// if for e.g. the beforListeners are empty,
// we have to ensure, that the original request
// is executed. This way the after handler will
// also be called properly.
if (filteredListeners.isEmpty() && handler != null) {
handler.handle(null);
}
}
/**
* This handler is called after the self request (original request) is performed
* successfully.
* The handler calls all listener (after), so this requests happen AFTER the original
* request is performed.
*
* @param ctx original request context
* @param buffer buffer
* @param afterListener list of listeners which should be called after the original request
* @return the after handler
*/
private Handler installAfterHandler(final RoutingContext ctx, final Buffer buffer, final List afterListener) {
return event -> callListener(ctx, buffer, afterListener, null);
}
/**
* This handler is called by the queueclient
* for each listener (before).
* The request happens BEFORE the original request is
* performed.
*
* @param ctx original request context
* @param buffer buffer
* @param beforeListener list of listeners which should be called before the original request
* @param afterHandler the handler for listeners which have to be called after the original request
* @return the before handler
*/
private Handler installBeforeHandler(final RoutingContext ctx, final Buffer buffer, final List beforeListener, final Handler afterHandler) {
return new Handler<>() {
private AtomicInteger currentCount = new AtomicInteger(0);
private boolean sent = false;
@Override
public void handle(Void event) {
// If the last queued request is performed
// the original request will be triggered.
// Because this handler is called async. we
// have to secure, that it is only executed
// once.
if ((currentCount.incrementAndGet() == beforeListener.size() || beforeListener.isEmpty()) && !sent) {
sent = true;
/*
* we should find exactly one or none route (first match rtl)
* routes will only be found for requests coming from
* enqueueing through the listener and only for external
* requests.
*/
Route route = routeRepository.getRoute(ctx.request().uri());
if (doMethodsMatch(route, ctx) && doHeadersMatch(route, ctx)) {
log.debug("Forward request (consumed) {}", ctx.request().uri());
route.forward(ctx, buffer, afterHandler);
} else {
// mark the original request as hooked
ctx.request().headers().set(HOOKED_HEADER, "true");
/*
* self requests are only made for original
* requests which were consumed during the
* enqueueing process, therefore it is
* imperative to use isRequestAlreadyHooked(HttpServerRequest request)
* before calling the handle method of
* this class!
*/
createSelfRequest(ctx.request(), buffer, afterHandler);
}
}
}
};
}
/**
* Returns a list with listeners which fires before / after the original request.
*
* @param listeners all listeners
* @return filtered listeners
*/
private List getFilteredListeners(final List listeners, final HookTriggerType hookTriggerType) {
return listeners.stream()
.filter(listener -> listener.getHook().getHookTriggerType().equals(hookTriggerType))
.collect(Collectors.toList());
}
/**
* This method is called after an incoming route
* unregistration is detected.
* This method deletes the route from the resource
* hookStorage.
*
* @param request request
*/
private void handleRouteUnregistration(final HttpServerRequest request) {
log.debug("handleRouteUnregistration > {}", request.uri());
// eg. /server/hooks/v1/registrations/+my+storage+id+
final String routeStorageUri = hookRootUri + HOOK_ROUTE_STORAGE_PATH + getStorageIdentifier(request.uri());
hookStorage.delete(routeStorageUri, status -> {
/*
* In case of an unregistration, it does not matter,
* if the route is still stored in the resource
* storage or not. It may even be the case, that the
* route has already expired and therefore vanished
* from the resource storage, but the cleanup job for the
* in-memory storage hasn't run yet.
* Even the service which calls the unregistration
* doesn't have to be notified if an unregistration
* 'fails', therefore always an OK status is sent.
*/
vertx.eventBus().publish(REMOVE_ROUTE_ADDRESS, request.uri());
request.response().end();
});
}
/**
* This method is called after an incoming route
* registration is detected.
* This method puts the registration request to the
* resource storage, so it can be reloaded even after
* a restart of the communication service.
* The request will be consumed in this process!
*
* @param request request
*/
private void handleRouteRegistration(final HttpServerRequest request) {
log.debug("handleRouteRegistration > {}", request.uri());
request.bodyHandler(hookData -> {
if (isHookJsonInvalid(request, hookData)) {
return;
}
// eg. /server/hooks/v1/registrations/+my+storage+id+
final String routeStorageUri = hookRootUri + HOOK_ROUTE_STORAGE_PATH + getStorageIdentifier(request.uri());
// Extract expireAfter from the registration header.
Integer expireAfter = ExpiryCheckHandler.getExpireAfter(request.headers());
if (expireAfter == null) {
expireAfter = DEFAULT_HOOK_STORAGE_EXPIRE_AFTER_TIME;
}
// Update the PUT header
ExpiryCheckHandler.setExpireAfter(request, expireAfter);
// calculate the expiration time for the listener / routes
DateTime expirationTime = ExpiryCheckHandler.getExpirationTime(expireAfter);
/*
* Create a new json object containing the request url
* and the hook itself.
* {
* "requesturl" : "http://...",
* "expirationTime" : "...",
* "hook" : { ... }
* }
*/
JsonObject hook;
try {
hook = new JsonObject(hookData.toString());
} catch (DecodeException e) {
badRequest(request, "Cannot decode JSON", e.getMessage());
return;
}
JsonObject storageObject = new JsonObject();
storageObject.put(REQUESTURL, request.uri());
storageObject.put(EXPIRATION_TIME, ExpiryCheckHandler.printDateTime(expirationTime));
storageObject.put(HOOK, hook);
Buffer buffer = Buffer.buffer(storageObject.toString());
hookStorage.put(routeStorageUri, request.headers(), buffer, status -> {
if (status == StatusCode.OK.getStatusCode()) {
if (logHookConfigurationResourceChanges) {
RequestLogger.logRequest(vertx.eventBus(), request, status, buffer);
}
vertx.eventBus().publish(SAVE_ROUTE_ADDRESS, routeStorageUri);
} else {
request.response().setStatusCode(status);
}
request.response().end();
});
});
}
/**
* Returns the identifier of the hook (only route) used in the
* resource storage.
* For listener identifiere take a look at getUniqueListenerId(...)
.
*
* @param url
* @return identifier
*/
private String getStorageIdentifier(String url) {
return url.replace("/", "+");
}
/**
* This method is called after an incoming listener
* unregistration is detected.
* This method deletes the listener from the resource
* storage.
*
* @param request request
*/
private void handleListenerUnregistration(final HttpServerRequest request) {
log.debug("handleListenerUnregistration > {}", request.uri());
// eg. /server/hooks/v1/registrations/listeners/http+myservice+1
final String listenerStorageUri = hookRootUri + HOOK_LISTENER_STORAGE_PATH + getUniqueListenerId(request.uri());
hookStorage.delete(listenerStorageUri, status -> {
/*
* In case of an unregistration, it does not matter,
* if the listener is still stored in the resource
* storage or not. It may even be the case, that the
* listener has already expired and therefore vanished
* from the resource storage, but the cleanup job for the
* in-memory storage hasn't run yet.
* Even the service which calls the unregistration
* doesn't have to be notified if an unregistration
* 'fails', therefore always an OK status is sent.
*/
vertx.eventBus().publish(REMOVE_LISTENER_ADDRESS, request.uri());
request.response().end();
});
}
/**
* This method is called after an incoming listener
* registration is detected.
* This method puts the registration request to the
* resource storage, so it can be reloaded even after
* a restart of the communication service.
* The request will be consumed in this process!
*
* @param request request
*/
private void handleListenerRegistration(final HttpServerRequest request) {
log.debug("handleListenerRegistration > {}", request.uri());
request.bodyHandler(hookData -> {
if (isListenerJsonInvalid(request, hookData)) {
return;
}
JsonObject hook;
try {
hook = new JsonObject(hookData);
} catch (DecodeException e) {
log.error("Cannot decode JSON", e);
badRequest(request, "Cannot decode JSON", e.getMessage());
return;
}
String destination = hook.getString(DESTINATION);
String hookOnUri = getMonitoredUrlSegment(request.uri());
if (destination.startsWith(hookOnUri)) {
badRequest(request, "illegal destination", "Destination-URI should not be within subtree of your hooked resource. This would lead to an infinite loop.");
return;
}
// eg. /server/hooks/v1/registrations/listeners/http+serviceName+hookId
final String listenerStorageUri = hookRootUri + HOOK_LISTENER_STORAGE_PATH + getUniqueListenerId(request.uri());
final String expirationTime = extractExpTimeAndManipulatePassedRequestAndReturnExpTime(request)
.orElse(null);
if (log.isDebugEnabled()) {
log.debug("Hook {} expirationTime is {}.", request.uri(), expirationTime);
}
/*
* Create a new json object containing the request url
* and the hook itself.
* {
* "requesturl" : "http://...",
* "expirationTime" : "...",
* "hook" : { ... }
* }
*/
JsonObject storageObject = new JsonObject();
storageObject.put(REQUESTURL, request.uri());
storageObject.put(EXPIRATION_TIME, expirationTime);
storageObject.put(HOOK, hook);
Buffer buffer = Buffer.buffer(storageObject.toString());
hookStorage.put(listenerStorageUri, request.headers(), buffer, status -> {
if (status == StatusCode.OK.getStatusCode()) {
if (logHookConfigurationResourceChanges) {
RequestLogger.logRequest(vertx.eventBus(), request, status, buffer);
}
vertx.eventBus().publish(SAVE_LISTENER_ADDRESS, listenerStorageUri);
} else {
request.response().setStatusCode(status);
}
request.response().end();
});
});
}
private boolean isListenerJsonInvalid(HttpServerRequest request, Buffer hookData) {
if (isHookJsonInvalid(request, hookData)) {
// No further checks required. hook definitively is invalid.
return true;
}
final JsonObject hook;
try {
// Badly we need to parse that JSON one more time.
hook = new JsonObject(hookData);
} catch (DecodeException e) {
log.error("Cannot decode JSON", e);
badRequest(request, "Cannot decode JSON", e.getMessage());
return true;
}
final JsonArray methods = hook.getJsonArray(METHODS);
if (methods != null) {
for (Object method : methods) {
if (!QueueProcessor.httpMethodIsQueueable(HttpMethod.valueOf((String) method))) {
final String msg = "Listener registration request tries to hook for not allowed '" + method + "' method.";
log.error(msg);
badRequest(request, "Bad Request", msg + "\n");
return true;
}
}
}
return false;
}
public boolean isHookJsonInvalid(HttpServerRequest request, Buffer hookData) {
try {
JsonNode hook = OBJECT_MAPPER.readTree(hookData.getBytes());
final Set valMsgs = jsonSchemaHook.validate(hook);
if (valMsgs.size() > 0) {
badRequest(request, "Hook JSON invalid", valMsgs.toString());
return true;
}
} catch (Exception ex) {
log.error("Cannot decode JSON", ex);
badRequest(request, "Cannot decode JSON", ex.getMessage());
return true;
}
return false;
}
private void badRequest(HttpServerRequest request, String statusMsg, String longMsg) {
HttpServerResponse response = request.response();
response.setStatusCode(StatusCode.BAD_REQUEST.getStatusCode());
request.response().setStatusMessage(statusMsg);
request.response().end(longMsg);
}
/**
* Creates a self Request from the original Request.
* If the requests succeeds (and only then) the after handler is called.
*
* @param request - consumed request
* @param requestBody - copy of request body
*/
private void createSelfRequest(final HttpServerRequest request, final Buffer requestBody, final Handler afterHandler) {
log.debug("Create self request for {}", request.uri());
selfClient.request(request.method(), request.uri()).onComplete(asyncReqResult -> {
if (asyncReqResult.failed()) {
log.warn("Failed request to {}: {}", request.uri(), asyncReqResult.cause());
return;
}
HttpClientRequest selfRequest = asyncReqResult.result();
if (request.headers() != null && !request.headers().isEmpty()) {
selfRequest.headers().setAll(request.headers());
}
selfRequest.exceptionHandler(exception -> log.warn("HookHandler HOOK_ERROR: Failed self request to {}: {}", request.uri(), exception.getMessage()));
selfRequest.idleTimeout(120000); // avoids blocking other requests
Handler> asyncResultHandler = asyncResult -> {
HttpClientResponse response = asyncResult.result();
/*
* it shouldn't matter if the request is
* already consumed to write a response.
*/
HttpServerRequestUtil.prepareResponse(request, response);
response.handler(data -> request.response().write(data));
response.endHandler(v -> request.response().end());
// if everything is fine, we call the after handler
if ((response.statusCode() / 100) == STATUS_CODE_2XX) {
afterHandler.handle(null);
}
};
if (requestBody != null) {
selfRequest.send(requestBody, asyncResultHandler);
} else {
selfRequest.send(asyncResultHandler);
}
});
}
/**
* Checks if the original Request was already hooked.
* Eg. After a request is processed by the hook handler
* (register), the handler creates a self request with
* a copy of the original request. Therefore it's
* necessary to mark the request as already hooked.
*
* @param request request
* @return true if the original request was already hooked.
*/
public boolean isRequestAlreadyHooked(HttpServerRequest request) {
String hooked = request.headers().get(HOOKED_HEADER);
return hooked != null && hooked.equals("true");
}
/**
* Removes the route from the repository.
*
* @param requestUrl requestUrl
*/
private void unregisterRoute(String requestUrl) {
String routedUrl = getRoutedUrlSegment(requestUrl);
log.debug("Unregister route {}", routedUrl);
routeRepository.removeRoute(routedUrl);
monitoringHandler.updateRoutesCount(routeRepository.getRoutes().size());
}
/**
* Removes the listener and its route from the repository.
*
* @param requestUrl
*/
private void unregisterListener(String requestUrl) {
String listenerId = getUniqueListenerId(requestUrl);
log.debug("Unregister listener {}", listenerId);
routeRepository.removeRoute(hookRootUri + LISTENER_HOOK_TARGET_PATH + getListenerUrlSegment(requestUrl));
listenerRepository.removeListener(listenerId);
monitoringHandler.updateListenerCount(listenerRepository.size());
}
/**
* Registers or updates an already existing listener and
* creates the necessary forwarder depending on the hook resource.
*
* @param buffer buffer
*/
@SuppressWarnings("unchecked")
private void registerListener(Buffer buffer) {
JsonObject storageObject = new JsonObject(buffer.toString());
String requestUrl = storageObject.getString(REQUESTURL);
if (log.isTraceEnabled()) {
log.trace("Request URL: {}", requestUrl);
}
// target = "http/colin/1234578" or destination url for internal forwarder (set later by if statement)
String target = getListenerUrlSegment(requestUrl);
// needed to identify listener
String listenerId = getUniqueListenerId(requestUrl);
if (log.isTraceEnabled()) {
log.trace("Target (1st): {}", target);
}
// create and add a new Forwarder (or replace an already existing forwarder)
JsonObject jsonHook = storageObject.getJsonObject(HOOK);
JsonArray jsonMethods = jsonHook.getJsonArray(METHODS);
HttpHook hook = new HttpHook(jsonHook.getString(DESTINATION));
if (jsonMethods != null) {
hook.setMethods(jsonMethods.getList());
}
String headersFilter = jsonHook.getString(HEADERS_FILTER);
if (headersFilter != null) {
try {
Pattern headersFilterPattern = RegexpValidator.throwIfPatternInvalid(headersFilter);
hook.setHeadersFilterPattern(headersFilterPattern);
} catch (ValidationException e) {
log.warn("Listener {} for target {} has an invalid headersFilter expression {} and will not be registered!",
listenerId, target, headersFilter);
return;
}
}
JsonObject jsonTranslateStatus = jsonHook.getJsonObject(TRANSLATE_STATUS);
if (jsonTranslateStatus != null) {
for (String pattern : jsonTranslateStatus.fieldNames()) {
hook.addTranslateStatus(Pattern.compile(pattern), jsonTranslateStatus.getInteger(pattern));
}
}
if (jsonHook.containsKey(FILTER)) {
hook.setFilter(jsonHook.getString(FILTER));
}
if (jsonHook.getInteger(QUEUE_EXPIRE_AFTER) != null) {
hook.setQueueExpireAfter(jsonHook.getInteger(QUEUE_EXPIRE_AFTER));
}
if (jsonHook.getString(HOOK_TRIGGER_TYPE) != null) {
try {
hook.setHookTriggerType(HookTriggerType.valueOf(jsonHook.getString(HOOK_TRIGGER_TYPE).toUpperCase()));
} catch (IllegalArgumentException e) {
log.warn("Listener " + listenerId + " for target " + target + " has an invalid trigger type " +
jsonHook.getString(HOOK_TRIGGER_TYPE) + " and will not be registered!", e);
return;
}
}
extractAndAddStaticHeadersToHook(jsonHook, hook);
extractAndAddProxyOptionsToHook(jsonHook, hook);
{ // Set expiration time
final String expirationTimeExpression = storageObject.getString(EXPIRATION_TIME);
if (expirationTimeExpression == null) {
log.debug("Register listener and route {} with infinite expiration.", target);
hook.setExpirationTime(null);
} else {
DateTime expirationTime;
try {
expirationTime = ExpiryCheckHandler.parseDateTime(expirationTimeExpression);
} catch (RuntimeException e) {
log.warn("Listener " + listenerId + " for target " + target + " has an invalid expiration time " +
expirationTimeExpression + " and will not be registered!", e);
return;
}
log.debug("Register listener and route {} with expiration at {}", target, expirationTime);
hook.setExpirationTime(expirationTime);
}
}
hook.setFullUrl(jsonHook.getBoolean(FULL_URL, false));
hook.setQueueingStrategy(QueueingStrategyFactory.buildQueueStrategy(jsonHook));
// for internal use we don't need a forwarder
if (hook.getDestination().startsWith("/")) {
if (log.isTraceEnabled()) {
log.trace("internal target, switching target!");
}
target = hook.getDestination();
} else {
String urlPattern = hookRootUri + LISTENER_HOOK_TARGET_PATH + target;
routeRepository.addRoute(urlPattern, createRoute(urlPattern, hook));
if (log.isTraceEnabled()) {
log.trace("external target, add route for urlPattern: {}", urlPattern);
}
}
if (log.isTraceEnabled()) {
log.trace("Target (2nd): {}", target);
}
// create and add a new listener (or update an already existing listener)
listenerRepository.addListener(new Listener(listenerId, getMonitoredUrlSegment(requestUrl), target, hook));
monitoringHandler.updateListenerCount(listenerRepository.size());
}
/**
* Extract proxyOptions attribute from jsonHook and create a
* appropriate property in the hook object.
*
* This is the same concept as in gateleen-routing:
* {@link org.swisspush.gateleen.routing.RuleFactory#setProxyOptions(Rule, JsonObject)}}
*/
private void extractAndAddProxyOptionsToHook(final JsonObject jsonHook, final HttpHook hook) {
JsonObject proxyOptions = jsonHook.getJsonObject("proxyOptions");
if (proxyOptions != null) {
hook.setProxyOptions(new ProxyOptions(proxyOptions));
}
}
/**
* Extract staticHeaders attribute from jsonHook and create a
* appropriate list in the hook object.
*
* This is the same concept as in gateleen-routing:
* {@link org.swisspush.gateleen.routing.RuleFactory#setStaticHeaders(Rule, JsonObject)}}
*/
private void extractAndAddStaticHeadersToHook(final JsonObject jsonHook, final HttpHook hook) {
final JsonArray headers = jsonHook.getJsonArray("headers");
if (headers != null) {
final HeaderFunction headerFunction = HeaderFunctions.parseFromJson(headers);
hook.setHeaderFunction(headerFunction);
return;
}
// {@see org.swisspush.gateleen.routing.RuleFactory.setStaticHeaders()}
// in previous Gateleen versions we only had the "staticHeaders" to unconditionally add headers with fix values
// We now have a more dynamic concept of a "manipulator chain" - which is also configured different in JSON syntax
// For backward compatibility we still parse the old "staticHeaders" - but now create a manipulator chain accordingly
JsonObject staticHeaders = jsonHook.getJsonObject(STATIC_HEADERS);
if (staticHeaders != null) {
log.warn("you use the deprecated \"staticHeaders\" syntax in your hook ({}). Please migrate to the more flexible \"headers\" syntax", jsonHook);
hook.setHeaderFunction(HeaderFunctions.parseStaticHeadersFromJson(staticHeaders));
}
}
/**
* Creates a listener id, which is unique for the given service, and the
* monitored url.
*
* @param requestUrl requestUrl
* @return String
*/
protected String getUniqueListenerId(String requestUrl) {
StringBuilder listenerId = new StringBuilder();
// eg. http/colin/1 -> http+colin+1
listenerId.append(convertToStoragePattern(getListenerUrlSegment(requestUrl)));
// eg. /gateleen/trip/v1 -> +gateleen+trip+v1
listenerId.append(convertToStoragePattern(getMonitoredUrlSegment(requestUrl)));
return listenerId.toString();
}
/**
* Replaces all unwanted charakters (like "/", ".", ":") with "+".
*
* @param urlSegment urlSegment
* @return String
*/
private String convertToStoragePattern(String urlSegment) {
return urlSegment.replace("/", "+").replace(".", "+").replace(":", "+");
}
/**
* Registers or updates an already existing route and
* creates the necessary forwarder depending on the hook resource.
*
* @param buffer buffer
*/
@SuppressWarnings("unchecked")
private void registerRoute(Buffer buffer) {
JsonObject storageObject = new JsonObject(buffer.toString());
String requestUrl = storageObject.getString(REQUESTURL);
String routedUrl = getRoutedUrlSegment(requestUrl);
log.debug("Register route to {}", routedUrl);
// create and add a new Forwarder (or replace an already existing forwarder)
JsonObject jsonHook = storageObject.getJsonObject(HOOK);
JsonArray jsonMethods = jsonHook.getJsonArray(METHODS);
HttpHook hook = new HttpHook(jsonHook.getString(DESTINATION));
if (jsonMethods != null) {
hook.setMethods(jsonMethods.getList());
}
String headersFilter = jsonHook.getString(HEADERS_FILTER);
if (headersFilter != null) {
try {
Pattern headersFilterPattern = RegexpValidator.throwIfPatternInvalid(headersFilter);
hook.setHeadersFilterPattern(headersFilterPattern);
} catch (ValidationException e) {
log.warn("Route {} has an invalid headersFilter expression {} and will not be registered!", routedUrl, headersFilter);
return;
}
}
JsonObject jsonTranslateStatus = jsonHook.getJsonObject(TRANSLATE_STATUS);
if (jsonTranslateStatus != null) {
for (String pattern : jsonTranslateStatus.fieldNames()) {
hook.addTranslateStatus(Pattern.compile(pattern), jsonTranslateStatus.getInteger(pattern));
}
}
if (jsonHook.getInteger(QUEUE_EXPIRE_AFTER) != null) {
hook.setQueueExpireAfter(jsonHook.getInteger(QUEUE_EXPIRE_AFTER));
}
if (jsonHook.getBoolean(LISTABLE) != null) {
hook.setListable(jsonHook.getBoolean(LISTABLE));
} else {
hook.setListable(listableRoutes);
}
if (jsonHook.getBoolean(COLLECTION) != null) {
hook.setCollection(jsonHook.getBoolean(COLLECTION));
}
extractAndAddStaticHeadersToHook(jsonHook, hook);
extractAndAddProxyOptionsToHook(jsonHook, hook);
/*
* Despite the fact, that every hook
* should have an expiration time,
* we check if the value is present.
*/
String expirationTimeExpression = storageObject.getString(EXPIRATION_TIME);
if (expirationTimeExpression != null) {
try {
hook.setExpirationTime(ExpiryCheckHandler.parseDateTime(expirationTimeExpression));
} catch (Exception e) {
log.warn("Route {} has an invalid expiration time {} and will not be registered!", routedUrl, expirationTimeExpression);
return;
}
} else {
log.warn("Route {} has no expiration time and will not be registered!", routedUrl);
return;
}
hook.setFullUrl(storageObject.getBoolean(FULL_URL, false));
hook.setQueueingStrategy(QueueingStrategyFactory.buildQueueStrategy(storageObject));
// Configure connection pool size
Integer originalPoolSize = jsonHook.getInteger(HttpHook.CONNECTION_POOL_SIZE_PROPERTY_NAME);
int appliedPoolSize;
if (originalPoolSize != null) {
appliedPoolSize = RuleFactory.evaluatePoolSize(originalPoolSize, routeMultiplier);
log.debug("Original pool size is {}, applied size is {}", originalPoolSize, appliedPoolSize);
hook.setConnectionPoolSize(appliedPoolSize);
}
hook.setMaxWaitQueueSize(jsonHook.getInteger(HttpHook.CONNECTION_MAX_WAIT_QUEUE_SIZE_PROPERTY_NAME));
// Configure request timeout
Integer timeout = jsonHook.getInteger(HttpHook.CONNECTION_TIMEOUT_SEC_PROPERTY_NAME);
if (timeout != null) {
hook.setTimeout(1000 * timeout);
}
boolean mustCreateNewRoute = true;
Route existingRoute = routeRepository.getRoutes().get(routedUrl);
if (existingRoute != null) {
mustCreateNewRoute = mustCreateNewRouteForHook(existingRoute, hook);
}
if (mustCreateNewRoute) {
routeRepository.addRoute(routedUrl, createRoute(routedUrl, hook));
} else {
// see comment in #mustCreateNewRouteForHook()
existingRoute.getRule().setHeaderFunction(hook.getHeaderFunction());
existingRoute.getHook().setExpirationTime(hook.getExpirationTime().orElse(null));
}
monitoringHandler.updateRoutesCount(routeRepository.getRoutes().size());
}
/**
* check if an existing route must be thrown away because the new Hook does not match the config of the existing Route
*
* @param existingRoute
* @param newHook
* @return true if something is different between old existing Route and new hook
*/
private boolean mustCreateNewRouteForHook(Route existingRoute, HttpHook newHook) {
HttpHook oldHook = existingRoute.getHook();
boolean same;
same = Objects.equals(oldHook.getDestination(), newHook.getDestination());
same &= Objects.equals(oldHook.getMethods(), newHook.getMethods());
same &= Objects.equals(oldHook.getTranslateStatus(), newHook.getTranslateStatus());
same &= oldHook.isCollection() == newHook.isCollection();
same &= oldHook.isFullUrl() == newHook.isFullUrl();
same &= oldHook.isListable() == newHook.isListable();
same &= oldHook.isCollection() == newHook.isCollection();
same &= oldHook.isCollection() == newHook.isCollection();
same &= Objects.equals(oldHook.getConnectionPoolSize(), newHook.getConnectionPoolSize());
same &= Objects.equals(oldHook.getMaxWaitQueueSize(), newHook.getMaxWaitQueueSize());
same &= Objects.equals(oldHook.getTimeout(), newHook.getTimeout());
same &= headersFilterPatternEquals(oldHook.getHeadersFilterPattern(), newHook.getHeadersFilterPattern());
// queueingStrategy, filter, queueExpireAfter and hookTriggerType are not relevant for Route-Hooks
// Though, headerFunction WOULD BE relevant - but we can't compare them for equality
// so we simply set the new HeaderFunction to the exising Rule
return !same;
}
private boolean headersFilterPatternEquals(Pattern headersFilterPatternLeft, Pattern headersFilterPatternRight) {
if (headersFilterPatternLeft != null && headersFilterPatternRight != null) {
return Objects.equals(headersFilterPatternLeft.pattern(), headersFilterPatternRight.pattern());
}
return headersFilterPatternLeft == null && headersFilterPatternRight == null;
}
/**
* Creates a new dynamic routing for the given hook.
*
* @param urlPattern urlPattern
* @param hook hook
* @return Route
*/
private Route createRoute(String urlPattern, HttpHook hook) {
return new Route(vertx, userProfileStorage, loggingResourceManager, logAppenderRepository, monitoringHandler,
userProfilePath, hook, urlPattern, selfClient);
}
/**
* Returns the url segment to which the route should be hooked.
* For "http://a/b/c/_hooks/route" this would
* be "http://a/b/c".
*
* @param requestUrl requestUrl
* @return url segment which requests should be routed
*/
private String getRoutedUrlSegment(String requestUrl) {
return requestUrl.substring(0, requestUrl.indexOf(HOOKS_ROUTE_URI_PART));
}
/**
* Returns the url segment to which the listener should be hooked.
* For "http://a/b/c/_hooks/listeners/http/colin/1234578" this would
* be "http://a/b/c".
*
* @param requestUrl requestUrl
* @return url segment to which the listener should be hooked.
*/
private String getMonitoredUrlSegment(String requestUrl) {
return requestUrl.substring(0, requestUrl.indexOf(HOOKS_LISTENERS_URI_PART));
}
/**
* Returns the url segment which represents the listener.
* For "http://a/b/c/_hooks/listeners/http/colin/1234578" this would
* be "http/colin/1234578".
*
* @param requestUrl requestUrl
* @return url segment
*/
private String getListenerUrlSegment(String requestUrl) {
// find the /_hooks/listeners/ identifier ...
int pos = requestUrl.indexOf(HOOKS_LISTENERS_URI_PART);
// ... and use substring after it as segment
return requestUrl.substring(pos + HOOKS_LISTENERS_URI_PART.length());
}
/**
* @param request Request to extract the value from. This instance gets manipulated
* internally during call.
* @return Expiration time or empty if infinite.
*/
private static Optional extractExpTimeAndManipulatePassedRequestAndReturnExpTime(HttpServerRequest request) {
final int expireAfter = ExpiryCheckHandler.getExpireAfterConcerningCaseOfCorruptHeaderAndInfinite(request.headers())
.orElse(DEFAULT_HOOK_STORAGE_EXPIRE_AFTER_TIME);
final String expirationTime = ExpiryCheckHandler.getExpirationTimeAsString(expireAfter)
.orElse(null);
// Update the PUT header
ExpiryCheckHandler.setExpireAfter(request, expireAfter);
return Optional.ofNullable(expirationTime);
}
}