package org.swisspush.redisques.handler; import com.google.common.collect.Ordering; import io.vertx.core.AsyncResult; import io.vertx.core.Handler; import io.vertx.core.Vertx; import io.vertx.core.eventbus.EventBus; import io.vertx.core.eventbus.Message; import io.vertx.core.http.HttpServerOptions; import io.vertx.core.http.HttpServerRequest; import io.vertx.core.http.HttpServerResponse; import io.vertx.core.json.DecodeException; import io.vertx.core.json.JsonArray; import io.vertx.core.json.JsonObject; import io.vertx.core.logging.Logger; import io.vertx.core.logging.LoggerFactory; import io.vertx.ext.web.Router; import io.vertx.ext.web.RoutingContext; import org.swisspush.redisques.util.RedisquesConfiguration; import org.swisspush.redisques.util.StatusCode; import java.nio.charset.Charset; import java.util.*; import java.util.concurrent.atomic.AtomicInteger; import static org.swisspush.redisques.util.RedisquesAPI.*; /** * Handler class for HTTP requests providing access to Redisques over HTTP. * * @author https://github.com/mcweba [Marc-Andre Weber] */ public class RedisquesHttpRequestHandler implements Handler<HttpServerRequest> { private static final String UTF_8 = "UTF-8"; private static Logger log = LoggerFactory.getLogger(RedisquesHttpRequestHandler.class); private Router router; private EventBus eventBus; private static final String APPLICATION_JSON = "application/json"; private static final String CONTENT_TYPE = "content-type"; private final String redisquesAddress; private final String userHeader; public static void init(Vertx vertx, RedisquesConfiguration modConfig){ log.info("Enable http request handler: " + modConfig.getHttpRequestHandlerEnabled()); if(modConfig.getHttpRequestHandlerEnabled()){ if(modConfig.getHttpRequestHandlerPort() != null && modConfig.getHttpRequestHandlerUserHeader() != null){ RedisquesHttpRequestHandler handler = new RedisquesHttpRequestHandler(vertx, modConfig); // in Vert.x 2x 100-continues was activated per default, in vert.x 3x it is off per default. HttpServerOptions options = new HttpServerOptions().setHandle100ContinueAutomatically(true); vertx.createHttpServer(options).requestHandler(handler).listen(modConfig.getHttpRequestHandlerPort(), result -> { if(result.succeeded()){ log.info("Successfully started http request handler on port " + modConfig.getHttpRequestHandlerPort()); } else { log.error("Unable to start http request handler. Message: " + result.cause().getMessage()); } }); } else { log.error("Configured to enable http request handler but no port configuration and/or user header configuration provided"); } } } private RedisquesHttpRequestHandler(Vertx vertx, RedisquesConfiguration modConfig) { this.router = Router.router(vertx); this.eventBus = vertx.eventBus(); this.redisquesAddress = modConfig.getAddress(); this.userHeader = modConfig.getHttpRequestHandlerUserHeader(); final String prefix = modConfig.getHttpRequestHandlerPrefix(); /* * List endpoints */ router.get(prefix + "/").handler(this::listEndpoints); /* * Get monitor information */ router.get(prefix + "/monitor/").handler(this::getMonitorInformation); /* * List queue items */ router.getWithRegex(prefix + "/monitor/[^/]+").handler(this::listQueueItems); /* * List or count queues */ router.get(prefix + "/queues/").handler(this::listOrCountQueues); /* * List or count queue items */ router.getWithRegex(prefix + "/queues/[^/]+").handler(this::listOrCountQueueItems); /* * Delete all queue items */ router.deleteWithRegex(prefix + "/queues/[^/]+").handler(this::deleteAllQueueItems); /* * Get single queue item */ router.getWithRegex(prefix + "/queues/([^/]+)/[0-9]+").handler(this::getSingleQueueItem); /* * Replace single queue item */ router.putWithRegex(prefix + "/queues/([^/]+)/[0-9]+").handler(this::replaceSingleQueueItem); /* * Delete single queue item */ router.deleteWithRegex(prefix + "/queues/([^/]+)/[0-9]+").handler(this::deleteQueueItem); /* * Add queue item */ router.postWithRegex(prefix + "/queues/([^/]+)/").handler(this::addQueueItem); /* * Get all locks */ router.getWithRegex(prefix + "/locks/").handler(this::getAllLocks); /* * Add lock */ router.putWithRegex(prefix + "/locks/[^/]+").handler(this::addLock); /* * Get single lock */ router.getWithRegex(prefix + "/locks/[^/]+").handler(this::getSingleLock); /* * Delete single lock */ router.deleteWithRegex(prefix + "/locks/[^/]+").handler(this::deleteSingleLock); router.routeWithRegex(".*").handler(this::respondMethodNotAllowed); } @Override public void handle(HttpServerRequest request) { router.accept(request); } private void respondMethodNotAllowed(RoutingContext ctx) { respondWith(StatusCode.METHOD_NOT_ALLOWED, ctx.request()); } private void listEndpoints(RoutingContext ctx) { JsonObject result = new JsonObject(); JsonArray items = new JsonArray(); items.add("locks/"); items.add("queues/"); items.add("monitor/"); result.put(lastPart(ctx.request().path(), "/"), items); ctx.response().putHeader(CONTENT_TYPE, APPLICATION_JSON); ctx.response().end(result.encode()); } private void getAllLocks(RoutingContext ctx) { eventBus.send(redisquesAddress, buildGetAllLocksOperation(), new Handler<AsyncResult<Message<JsonObject>>>() { @Override public void handle(AsyncResult<Message<JsonObject>> reply) { if (OK.equals(reply.result().body().getString(STATUS))) { jsonResponse(ctx.response(), reply.result().body().getJsonObject(VALUE)); } else { respondWith(StatusCode.NOT_FOUND, ctx.request()); } } }); } private void addLock(RoutingContext ctx) { String queue = lastPart(ctx.request().path(), "/"); eventBus.send(redisquesAddress, buildPutLockOperation(queue, extractUser(ctx.request())), new Handler<AsyncResult<Message<JsonObject>>>() { @Override public void handle(AsyncResult<Message<JsonObject>> reply) { checkReply(reply.result(), ctx.request(), StatusCode.BAD_REQUEST); } }); } private void getSingleLock(RoutingContext ctx) { String queue = lastPart(ctx.request().path(), "/"); eventBus.send(redisquesAddress, buildGetLockOperation(queue), new Handler<AsyncResult<Message<JsonObject>>>() { @Override public void handle(AsyncResult<Message<JsonObject>> reply) { if (OK.equals(reply.result().body().getString(STATUS))) { ctx.response().putHeader(CONTENT_TYPE, APPLICATION_JSON); ctx.response().end(reply.result().body().getString(VALUE)); } else { ctx.response().setStatusCode(StatusCode.NOT_FOUND.getStatusCode()); ctx.response().setStatusMessage(StatusCode.NOT_FOUND.getStatusMessage()); ctx.response().end(NO_SUCH_LOCK); } } }); } private void deleteSingleLock(RoutingContext ctx) { String queue = lastPart(ctx.request().path(), "/"); eventBus.send(redisquesAddress, buildDeleteLockOperation(queue), new Handler<AsyncResult<Message<JsonObject>>>() { @Override public void handle(AsyncResult<Message<JsonObject>> reply) { checkReply(reply.result(), ctx.request(), StatusCode.INTERNAL_SERVER_ERROR); } }); } private void getQueuesCount(RoutingContext ctx) { eventBus.send(redisquesAddress, buildGetQueuesCountOperation(), new Handler<AsyncResult<Message<JsonObject>>>() { @Override public void handle(AsyncResult<Message<JsonObject>> reply) { if (reply.succeeded() && OK.equals(reply.result().body().getString(STATUS))) { JsonObject result = new JsonObject(); result.put("count", reply.result().body().getLong(VALUE)); jsonResponse(ctx.response(), result); } else { respondWith(StatusCode.INTERNAL_SERVER_ERROR, "Error gathering count of active queues", ctx.request()); } } }); } private void getQueueItemsCount(RoutingContext ctx) { final String queue = lastPart(ctx.request().path(), "/"); eventBus.send(redisquesAddress, buildGetQueueItemsCountOperation(queue), new Handler<AsyncResult<Message<JsonObject>>>() { @Override public void handle(AsyncResult<Message<JsonObject>> reply) { if (reply.succeeded() && OK.equals(reply.result().body().getString(STATUS))) { JsonObject result = new JsonObject(); result.put("count", reply.result().body().getLong(VALUE)); jsonResponse(ctx.response(), result); } else { respondWith(StatusCode.INTERNAL_SERVER_ERROR, "Error gathering count of active queue items", ctx.request()); } } }); } private void getMonitorInformation(RoutingContext ctx){ boolean emptyQueues = ctx.request().params().contains("emptyQueues"); final JsonObject resultObject = new JsonObject(); final JsonArray queuesArray = new JsonArray(); eventBus.send(redisquesAddress, buildGetQueuesOperation(), new Handler<AsyncResult<Message<JsonObject>>>() { @Override public void handle(AsyncResult<Message<JsonObject>> reply) { if (reply.succeeded() && OK.equals(reply.result().body().getString(STATUS))) { final List<String> queueNames = reply.result().body().getJsonObject(VALUE).getJsonArray("queues").getList(); collectQueueLengths(queueNames, extractLimit(ctx), emptyQueues, mapEntries -> { for (Map.Entry<String, Long> entry : mapEntries) { JsonObject obj = new JsonObject(); obj.put("name", entry.getKey()); obj.put("size", entry.getValue()); queuesArray.add(obj); } resultObject.put("queues", queuesArray); jsonResponse(ctx.response(), resultObject); }); } else { String error = "Error gathering names of active queues"; log.error(error); respondWith(StatusCode.INTERNAL_SERVER_ERROR, error, ctx.request()); } }}); } private void listOrCountQueues(RoutingContext ctx) { if (ctx.request().params().contains("count")) { getQueuesCount(ctx); } else { listQueues(ctx); } } private void listQueues(RoutingContext ctx) { eventBus.send(redisquesAddress, buildGetQueuesOperation(), new Handler<AsyncResult<Message<JsonObject>>>() { @Override public void handle(AsyncResult<Message<JsonObject>> reply) { if (reply.succeeded() && OK.equals(reply.result().body().getString(STATUS))) { jsonResponse(ctx.response(), reply.result().body().getJsonObject(VALUE)); } else { respondWith(StatusCode.INTERNAL_SERVER_ERROR, "Error gathering names of active queues", ctx.request()); } } }); } private void listOrCountQueueItems(RoutingContext ctx) { if (ctx.request().params().contains("count")) { getQueueItemsCount(ctx); } else { listQueueItems(ctx); } } private void listQueueItems(RoutingContext ctx) { final String queue = lastPart(ctx.request().path(), "/"); String limitParam = null; if (ctx.request() != null && ctx.request().params().contains("limit")) { limitParam = ctx.request().params().get("limit"); } eventBus.send(redisquesAddress, buildGetQueueItemsOperation(queue, limitParam), new Handler<AsyncResult<Message<JsonObject>>>() { @Override public void handle(AsyncResult<Message<JsonObject>> reply) { JsonObject replyBody = reply.result().body(); if (OK.equals(replyBody.getString(STATUS))) { List<Object> list = reply.result().body().getJsonArray(VALUE).getList(); JsonArray items = new JsonArray(); for (Object item : list.toArray()) { items.add((String) item); } JsonObject result = new JsonObject().put(queue, items); jsonResponse(ctx.response(), result); } else { ctx.response().setStatusCode(StatusCode.NOT_FOUND.getStatusCode()); ctx.response().end(reply.result().body().getString("message")); log.warn("Error in routerMatcher.getWithRegEx. Command = '" + (replyBody.getString("command") == null ? "<null>" : replyBody.getString("command")) + "'."); } } }); } private void addQueueItem(RoutingContext ctx) { final String queue = part(ctx.request().path(), "/", 1); ctx.request().bodyHandler(buffer -> { try { String strBuffer = encode(buffer.toString()); eventBus.send(redisquesAddress, buildAddQueueItemOperation(queue, strBuffer), new Handler<AsyncResult<Message<JsonObject>>>() { @Override public void handle(AsyncResult<Message<JsonObject>> reply) { checkReply(reply.result(), ctx.request(), StatusCode.BAD_REQUEST); } }); } catch (Exception ex) { respondWith(StatusCode.BAD_REQUEST, ex.getMessage(), ctx.request()); } }); } private void getSingleQueueItem(RoutingContext ctx) { final String queue = lastPart(ctx.request().path().substring(0, ctx.request().path().length() - 2), "/"); final int index = Integer.parseInt(lastPart(ctx.request().path(), "/")); eventBus.send(redisquesAddress, buildGetQueueItemOperation(queue, index), new Handler<AsyncResult<Message<JsonObject>>>() { @Override public void handle(AsyncResult<Message<JsonObject>> reply) { JsonObject replyBody = reply.result().body(); if (OK.equals(replyBody.getString(STATUS))) { ctx.response().putHeader(CONTENT_TYPE, APPLICATION_JSON); ctx.response().end(decode(reply.result().body().getString(VALUE))); } else { ctx.response().setStatusCode(StatusCode.NOT_FOUND.getStatusCode()); ctx.response().setStatusMessage(StatusCode.NOT_FOUND.getStatusMessage()); ctx.response().end("Not Found"); } } }); } private void replaceSingleQueueItem(RoutingContext ctx){ final String queue = part(ctx.request().path(), "/", 2); checkLocked(queue, ctx.request(), aVoid -> { final int index = Integer.parseInt(lastPart(ctx.request().path(), "/")); ctx.request().bodyHandler(buffer -> { try { String strBuffer = encode(buffer.toString()); eventBus.send(redisquesAddress, buildReplaceQueueItemOperation(queue, index, strBuffer), new Handler<AsyncResult<Message<JsonObject>>>() { @Override public void handle(AsyncResult<Message<JsonObject>> reply) { checkReply(reply.result(), ctx.request(), StatusCode.NOT_FOUND); } }); } catch (Exception ex) { respondWith(StatusCode.BAD_REQUEST, ex.getMessage(), ctx.request()); } }); }); } private void deleteQueueItem(RoutingContext ctx) { final String queue = part(ctx.request().path(), "/", 2); final int index = Integer.parseInt(lastPart(ctx.request().path(), "/")); checkLocked(queue, ctx.request(), aVoid -> eventBus.send(redisquesAddress, buildDeleteQueueItemOperation(queue, index), new Handler<AsyncResult<Message<JsonObject>>>() { @Override public void handle(AsyncResult<Message<JsonObject>> reply) { checkReply(reply.result(), ctx.request(), StatusCode.NOT_FOUND); } })); } private void deleteAllQueueItems(RoutingContext ctx) { final String queue = lastPart(ctx.request().path(), "/"); eventBus.send(redisquesAddress, buildDeleteAllQueueItemsOperation(queue), reply -> { ctx.response().end(); }); } private void respondWith(StatusCode statusCode, String responseMessage, HttpServerRequest request) { log.info("Responding with status code " + statusCode + " and message: " + responseMessage); request.response().setStatusCode(statusCode.getStatusCode()); request.response().setStatusMessage(statusCode.getStatusMessage()); request.response().end(responseMessage); } private void respondWith(StatusCode statusCode, HttpServerRequest request) { respondWith(statusCode, statusCode.getStatusMessage(), request); } private String lastPart(String source, String separator) { String[] tokens = source.split(separator); return tokens[tokens.length - 1]; } private String part(String source, String separator, int pos) { String[] tokens = source.split(separator); return tokens[tokens.length - pos]; } private void jsonResponse(HttpServerResponse response, JsonObject object) { response.putHeader(CONTENT_TYPE, APPLICATION_JSON); response.end(object.encode()); } private String extractUser(HttpServerRequest request) { String user = request.headers().get(userHeader); if (user == null) { user = "Unknown"; } return user; } private void checkLocked(String queue, final HttpServerRequest request, final Handler<Void> handler) { request.pause(); eventBus.send(redisquesAddress, buildGetLockOperation(queue), new Handler<AsyncResult<Message<JsonObject>>>() { @Override public void handle(AsyncResult<Message<JsonObject>> reply) { if (NO_SUCH_LOCK.equals(reply.result().body().getString(STATUS))) { request.resume(); request.response().setStatusCode(StatusCode.CONFLICT.getStatusCode()); request.response().setStatusMessage("Queue must be locked to perform this operation"); request.response().end("Queue must be locked to perform this operation"); } else { handler.handle(null); request.resume(); } } }); } private void checkReply(Message<JsonObject> reply, HttpServerRequest request, StatusCode statusCode) { if (OK.equals(reply.body().getString(STATUS))) { request.response().end(); } else { request.response().setStatusCode(statusCode.getStatusCode()); request.response().setStatusMessage(statusCode.getStatusMessage()); request.response().end(statusCode.getStatusMessage()); } } /** * Encode the payload from a payloadString or payloadObjet. * * @param decoded decoded * @return String */ public String encode(String decoded) throws Exception { JsonObject object = new JsonObject(decoded); String payloadString; JsonObject payloadObject = object.getJsonObject("payloadObject"); if (payloadObject != null) { payloadString = payloadObject.encode(); } else { payloadString = object.getString("payloadString"); } if (payloadString != null) { object.put(PAYLOAD, payloadString.getBytes(Charset.forName(UTF_8))); object.remove("payloadString"); object.remove("payloadObject"); } // update the content-length int length = 0; if (object.containsKey(PAYLOAD)) { length = object.getBinary(PAYLOAD).length; } JsonArray newHeaders = new JsonArray(); for (Object headerObj : object.getJsonArray("headers")) { JsonArray header = (JsonArray) headerObj; String key = header.getString(0); if (key.equalsIgnoreCase("content-length")) { JsonArray contentLengthHeader = new JsonArray(); contentLengthHeader.add("Content-Length"); contentLengthHeader.add(Integer.toString(length)); newHeaders.add(contentLengthHeader); } else { newHeaders.add(header); } } object.put("headers", newHeaders); return object.toString(); } /** * Decode the payload if the content-type is text or json. * * @param encoded encoded * @return String */ public String decode(String encoded) { JsonObject object = new JsonObject(encoded); JsonArray headers = object.getJsonArray("headers"); for (Object headerObj : headers) { JsonArray header = (JsonArray) headerObj; String key = header.getString(0); String value = header.getString(1); if (key.equalsIgnoreCase(CONTENT_TYPE) && (value.contains("text/") || value.contains(APPLICATION_JSON))) { try { object.put("payloadObject", new JsonObject(new String(object.getBinary(PAYLOAD), Charset.forName(UTF_8)))); } catch (DecodeException e) { object.put("payloadString", new String(object.getBinary(PAYLOAD), Charset.forName(UTF_8))); } object.remove(PAYLOAD); break; } } return object.toString(); } private int extractLimit(RoutingContext ctx){ String limitParam = ctx.request().params().get("limit"); try{ return Integer.parseInt(limitParam); } catch (NumberFormatException ex){ if(limitParam != null){ log.warn("Non-numeric limit parameter value used: " + limitParam); } return Integer.MAX_VALUE; } } private void collectQueueLengths(final List<String> queueNames, final int limit, final boolean showEmptyQueues, final QueueLengthCollectingCallback callback) { final SortedMap<String, Long> resultMap = new TreeMap<>(); final List<Map.Entry<String, Long>> mapEntryList = new ArrayList<>(); final AtomicInteger subCommandCount = new AtomicInteger(queueNames.size()); if (!queueNames.isEmpty()) { for (final String name : queueNames) { eventBus.send(redisquesAddress, buildGetQueueItemsCountOperation(name), new Handler<AsyncResult<Message<JsonObject>>>() { @Override public void handle(AsyncResult<Message<JsonObject>> reply) { subCommandCount.decrementAndGet(); if (reply.succeeded() && OK.equals(reply.result().body().getString(STATUS))) { final long count = reply.result().body().getLong(VALUE); if (showEmptyQueues || count > 0) { resultMap.put(name, count); } } else { log.error("Error gathering size of queue " + name); } if (subCommandCount.get() == 0) { mapEntryList.addAll(resultMap.entrySet()); sortResultMap(mapEntryList); int toIndex = limit > queueNames.size() ? queueNames.size() : limit; toIndex = Math.min(mapEntryList.size(), toIndex); callback.onDone(mapEntryList.subList(0, toIndex)); } } }); } } else { callback.onDone(mapEntryList); } } private interface QueueLengthCollectingCallback { void onDone(List<Map.Entry<String, Long>> mapEntries); } private void sortResultMap(List<Map.Entry<String, Long>> input) { Ordering<Map.Entry<String, Long>> byMapValues = new Ordering<Map.Entry<String, Long>>() { @Override public int compare(Map.Entry<String, Long> left, Map.Entry<String, Long> right) { return left.getValue().compareTo(right.getValue()); } }; Collections.sort(input, byMapValues.reverse()); } }