All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.swisspush.reststorage.RestStorageHandler Maven / Gradle / Ivy

package org.swisspush.reststorage;

import io.netty.util.internal.StringUtil;
import io.vertx.core.Handler;
import io.vertx.core.MultiMap;
import io.vertx.core.Vertx;
import io.vertx.core.http.HttpServerRequest;
import io.vertx.core.http.HttpServerResponse;
import io.vertx.core.json.JsonArray;
import io.vertx.core.json.JsonObject;
import io.vertx.core.streams.Pump;
import io.vertx.ext.auth.authentication.AuthenticationProvider;
import io.vertx.ext.web.Router;
import io.vertx.ext.web.RoutingContext;
import io.vertx.ext.web.handler.BasicAuthHandler;
import org.apache.commons.lang.StringUtils;
import org.slf4j.Logger;
import org.swisspush.reststorage.exception.RestStorageExceptionFactory;
import org.swisspush.reststorage.util.*;

import java.text.DecimalFormat;
import java.util.*;

import static java.nio.charset.StandardCharsets.UTF_8;
import static org.swisspush.reststorage.util.HttpRequestHeader.*;
import static org.swisspush.reststorage.util.HttpRequestParam.getString;
import static org.swisspush.reststorage.util.HttpRequestParam.*;

public class RestStorageHandler implements Handler {

    private final Logger log;
    private final Router router;
    private final Storage storage;
    private final RestStorageExceptionFactory exceptionFactory;

    private final MimeTypeResolver mimeTypeResolver = new MimeTypeResolver("application/json; charset=utf-8");

    private final Map editors = new LinkedHashMap<>();

    private final String newMarker = "?new=true";
    private final String prefixFixed;
    private final String prefix;
    private final boolean confirmCollectionDelete;
    private final boolean rejectStorageWriteOnLowMemory;
    private final boolean return200onDeleteNonExisting;
    private final DecimalFormat decimalFormat;
    private final Integer maxStorageExpandSubresources;
    private final long configuredCleanupAmount;

    public RestStorageHandler(
        Vertx vertx,
        Logger log,
        Storage storage,
        RestStorageExceptionFactory exceptionFactory,
        ModuleConfiguration config
    ) {
        this.router = Router.router(vertx);
        this.log = log;
        this.storage = storage;
        this.exceptionFactory = exceptionFactory;
        this.prefix = config.getPrefix();
        this.confirmCollectionDelete = config.isConfirmCollectionDelete();
        this.rejectStorageWriteOnLowMemory = config.isRejectStorageWriteOnLowMemory();
        this.return200onDeleteNonExisting = config.isReturn200onDeleteNonExisting();
        this.maxStorageExpandSubresources = config.getMaxStorageExpandSubresources();
        this.configuredCleanupAmount = config.getResourceCleanupAmount();

        this.decimalFormat = new DecimalFormat();
        this.decimalFormat.setMaximumFractionDigits(1);

        prefixFixed = prefix.equals("/") ? "" : prefix;

        if (config.getEditorConfig() != null) {
            editors.putAll(config.getEditorConfig());
        }

        Result result = checkHttpAuthenticationConfiguration(config);
        if(result.isErr()) {
            router.route().handler(ctx -> {
                log.warn("stacktrace", exceptionFactory.newException(
                    "router.route() failed: " + result.getErr()));
                respondWith(ctx.response(), StatusCode.INTERNAL_SERVER_ERROR, result.getErr());
            });
        } else if (result.getOk()) {
            AuthenticationProvider authProvider = new ModuleConfigurationAuthentication(config);
                router.route().handler(BasicAuthHandler.create(authProvider));
            log.info("Authentication enabled for HTTP API");
        }

        router.postWithRegex(".*_cleanup").handler(this::cleanup);

        router.postWithRegex(prefixFixed + ".*").handler(this::storageExpand);

        router.getWithRegex(prefixFixed + ".*").handler(this::getResource);

        router.putWithRegex(prefixFixed + ".*").handler(this::putResource);

        router.deleteWithRegex(prefixFixed + ".*").handler(this::deleteResource);

        router.getWithRegex(".*").handler(this::getResourceNotFound);

        router.routeWithRegex(".*").handler(this::respondMethodNotAllowed);
    }

    @Override
    public void handle(HttpServerRequest request) {
        router.handle(request);
    }

    ////////////////////////////
    // Begin Router handling  //
    ////////////////////////////

    private void respondMethodNotAllowed(RoutingContext ctx) {
        respondWithNotAllowed(ctx.request());
    }

    private void cleanup(RoutingContext ctx) {
        log.trace("RestStorageHandler cleanup");
        storage.cleanup(documentResource -> {
            log.trace("RestStorageHandler cleanup");
            var rsp = ctx.response();
            rsp.headers().add(CONTENT_LENGTH.getName(), "" + documentResource.length);
            rsp.headers().add(CONTENT_TYPE.getName(), "application/json; charset=utf-8");
            rsp.setStatusCode(StatusCode.OK.getStatusCode());
            final Pump pump = Pump.pump(documentResource.readStream, rsp);
            documentResource.readStream.endHandler(nothing -> {
                documentResource.closeHandler.handle(null);
                ctx.response().end();
            });
            pump.start();
        }, getCleanupResourcesAmountContextOrConfig(ctx));
    }

    private String getCleanupResourcesAmountContextOrConfig(RoutingContext ctx) {
        String routingContextCleanupAmount = ctx.request().getParam("cleanupResourcesAmount");
        if (StringUtils.isNotEmpty(routingContextCleanupAmount)) {
            return routingContextCleanupAmount;
        }
        return String.valueOf(this.configuredCleanupAmount);
    }

    private void getResourceNotFound(RoutingContext ctx) {
        if (log.isTraceEnabled()) {
            log.trace("RestStorageHandler resource not found: {}", ctx.request().uri());
        }
        var rsp = ctx.response();
        rsp.setStatusCode(StatusCode.NOT_FOUND.getStatusCode());
        rsp.setStatusMessage(StatusCode.NOT_FOUND.getStatusMessage());
        rsp.end(StatusCode.NOT_FOUND.toString());
    }

    private void getResource(RoutingContext ctx) {
        final String path = cleanPath(ctx.request().path().substring(prefixFixed.length()));
        final String etag = ctx.request().headers().get(IF_NONE_MATCH_HEADER.getName());
        log.trace("RestStorageHandler got GET Request path: {} etag: {}", path, etag);
        MultiMap params = ctx.request().params();
        String offsetFromUrl = getString(params, OFFSET_PARAMETER);
        String limitFromUrl = getString(params, LIMIT_PARAMETER);
        OffsetLimit offsetLimit = UrlParser.offsetLimit(offsetFromUrl, limitFromUrl);
        storage.get(path, etag, offsetLimit.offset, offsetLimit.limit, new Handler<>() {
            public void handle(Resource resource) {
                log.trace("RestStorageHandler resource exists: {}", resource.exists);
                var rsp = ctx.response();

                if (resource.error) {
                    rsp.setStatusCode(StatusCode.INTERNAL_SERVER_ERROR.getStatusCode());
                    rsp.setStatusMessage(StatusCode.INTERNAL_SERVER_ERROR.getStatusMessage());
                    String message = StatusCode.INTERNAL_SERVER_ERROR.getStatusMessage();
                    if (resource.errorMessage != null) {
                        message = resource.errorMessage;
                    }
                    rsp.end(message);
                    return;
                }

                if (!resource.modified) {
                    rsp.setStatusCode(StatusCode.NOT_MODIFIED.getStatusCode());
                    rsp.setStatusMessage(StatusCode.NOT_MODIFIED.getStatusMessage());
                    rsp.headers().set(ETAG_HEADER.getName(), etag);
                    rsp.headers().add(CONTENT_LENGTH.getName(), "0");
                    rsp.end();
                    return;
                }

                var req = ctx.request();
                if (resource.exists) {
                    String accept = req.headers().get("Accept");
                    boolean html = (accept != null && accept.contains("text/html"));
                    if (resource instanceof CollectionResource) {
                        if (log.isTraceEnabled()) {
                            log.trace("RestStorageHandler resource is collection: {}", req.uri());
                        }
                        CollectionResource collection = (CollectionResource) resource;
                        String collectionName = collectionName(path);
                        if (html && !req.uri().endsWith("/")) {
                            log.trace("RestStorageHandler accept contains text/html and ends with /");
                            rsp.setStatusCode(StatusCode.FOUND.getStatusCode());
                            rsp.setStatusMessage(StatusCode.FOUND.getStatusMessage());
                            rsp.headers().add("Location", req.uri() + "/");
                            rsp.end();
                        } else if (html) {
                            log.trace("RestStorageHandler accept contains text/html");
                            if (!(req.query() != null && req.query().contains("follow=off")) &&
                                    collection.items.size() == 1 &&
                                    collection.items.get(0) instanceof CollectionResource) {
                                log.trace("RestStorageHandler query contains follow=off");
                                rsp.setStatusCode(StatusCode.FOUND.getStatusCode());
                                rsp.setStatusMessage(StatusCode.FOUND.getStatusMessage());
                                rsp.headers().add("Location", (req.uri()) + collection.items.get(0).name);
                                rsp.end();
                                return;
                            }

                            StringBuilder body = new StringBuilder(1024);
                            String editor = null;
                            if (!editors.isEmpty()) {
                                editor = editors.values().iterator().next();
                            }
                            body.append("\n");
                            body.append("").append(collectionName).append("");
                            body.append("");
                            body.append("
").append(htmlPath(prefix + path)).append("
"); if (editor != null) { String editorString = editor.replace("$path", path + (path.equals("/") ? "" : "/") + "$new"); editorString = editorString.replaceFirst("\\?", newMarker); body.append("
" + "
"); } body.append("
  • ..
  • "); List sortedNames = sortedNames(collection); ResourceNameUtil.resetReplacedColonsAndSemiColonsInList(sortedNames); for (String name : sortedNames) { body.append("
  • " + name + ""); body.append("
  • "); } body.append("
"); rsp.headers().add(CONTENT_LENGTH.getName(), "" + body.length()); rsp.headers().add(CONTENT_TYPE.getName(), "text/html; charset=utf-8"); rsp.end(body.toString()); } else { JsonArray array = new JsonArray(); List sortedNames = sortedNames(collection); ResourceNameUtil.resetReplacedColonsAndSemiColonsInList(sortedNames); sortedNames.forEach(array::add); log.trace("RestStorageHandler return collection: {}", sortedNames); String body = new JsonObject().put(collectionName, array).encode(); /* TODO Check implementation * Why do we use 'String#length()' here? Just imagine what happens if that JSON * contains any char above codepoint 127. */ if( log.isWarnEnabled() ){ int numChrs = body.length(); int numByts = body.getBytes(UTF_8).length; if( numChrs != numByts ){ log.warn("assert({} == {})", numChrs, numByts); } } assert body.length() == body.getBytes(UTF_8).length; /* BTW: code below for whatever reason seems to use count of codepoints instead * count of bytes (see checks above). */ rsp.headers().add(CONTENT_LENGTH.getName(), "" + body.length()); rsp.headers().add(CONTENT_TYPE.getName(), "application/json; charset=utf-8"); rsp.end(body); } } if (resource instanceof DocumentResource) { if (log.isTraceEnabled()) { log.trace("RestStorageHandler resource is a DocumentResource: {}", req.uri()); } if (req.uri().endsWith("/")) { log.trace("RestStorageHandler DocumentResource ends with /"); rsp.setStatusCode(StatusCode.FOUND.getStatusCode()); rsp.setStatusMessage(StatusCode.FOUND.getStatusMessage()); rsp.headers().add("Location", req.uri().substring(0, req.uri().length() - 1)); rsp.end(); } else { log.trace("RestStorageHandler DocumentResource does not end with /"); String mimeType = mimeTypeResolver.resolveMimeType(path); if (req.headers().names().contains("Accept") && req.headers().get("Accept").contains("text/html")) { String editor = editors.get(mimeType.split(";")[0]); if (editor != null) { rsp.setStatusCode(StatusCode.FOUND.getStatusCode()); rsp.setStatusMessage(StatusCode.FOUND.getStatusMessage()); String editorString = editor.replaceAll("\\$path", path); rsp.headers().add("Location", editorString); rsp.end(); return; } } final DocumentResource documentResource = (DocumentResource) resource; if (documentResource.etag != null && !documentResource.etag.isEmpty()) { rsp.headers().add(ETAG_HEADER.getName(), documentResource.etag); } rsp.headers().add(CONTENT_LENGTH.getName(), "" + documentResource.length); rsp.headers().add(CONTENT_TYPE.getName(), mimeType); final Pump pump = Pump.pump(documentResource.readStream, rsp); documentResource.readStream.endHandler(nothing -> { documentResource.closeHandler.handle(null); rsp.end(); }); documentResource.addErrorHandler((Throwable ex) -> log.error("stacktrace", exceptionFactory.newException("TODO error handling", ex))); documentResource.readStream.exceptionHandler((Throwable ex) -> log.error("stacktrace", exceptionFactory.newException("TODO error handling", ex))); pump.start(); } } } else { if (log.isTraceEnabled()) { log.trace("RestStorageHandler Could not find resource: {}", req.uri()); } rsp.setStatusCode(StatusCode.NOT_FOUND.getStatusCode()); rsp.setStatusMessage(StatusCode.NOT_FOUND.getStatusMessage()); rsp.end(StatusCode.NOT_FOUND.toString()); } } private List sortedNames(CollectionResource collection) { List collections = new ArrayList<>(collection.items.size()); List documents = new ArrayList<>(); for (Resource r : collection.items) { String name = r.name; if (r instanceof CollectionResource) { collections.add(name + "/"); } else { documents.add(name); } } collections.addAll(documents); return collections; } }); } private void putResource(RoutingContext ctx) { var req = ctx.request(); var rsp = ctx.response(); req.pause(); final String path = cleanPath(req.path().substring(prefixFixed.length())); MultiMap headers = req.headers(); Integer importanceLevel; if (containsHeader(headers, IMPORTANCE_LEVEL_HEADER)) { importanceLevel = getInteger(headers, IMPORTANCE_LEVEL_HEADER); if (importanceLevel == null) { req.resume(); rsp.setStatusCode(StatusCode.BAD_REQUEST.getStatusCode()); rsp.setStatusMessage(StatusCode.BAD_REQUEST.getStatusMessage()); rsp.end("Invalid " + IMPORTANCE_LEVEL_HEADER.getName() + " header: " + headers.get(IMPORTANCE_LEVEL_HEADER.getName())); log.error("Rejecting PUT request to {} because {} header, has an invalid value: {}", req.uri(), IMPORTANCE_LEVEL_HEADER.getName(), headers.get(IMPORTANCE_LEVEL_HEADER.getName())); return; } if (rejectStorageWriteOnLowMemory) { Optional currentMemoryUsage = storage.getCurrentMemoryUsage(); if (currentMemoryUsage.isPresent()) { if (currentMemoryUsage.get() > importanceLevel) { req.resume(); rsp.setStatusCode(StatusCode.INSUFFICIENT_STORAGE.getStatusCode()); rsp.setStatusMessage(StatusCode.INSUFFICIENT_STORAGE.getStatusMessage()); rsp.end(StatusCode.INSUFFICIENT_STORAGE.getStatusMessage()); log.info("Rejecting PUT request to {} because current memory usage of {}% is higher than " + "provided importance level of {}%", req.uri(), decimalFormat.format(currentMemoryUsage.get()), importanceLevel); return; } } else { log.warn("Rejecting storage writes on low memory feature disabled, because current memory usage not available"); } } else { log.warn("Received request with {} header, but rejecting storage writes on low memory feature " + "is disabled", IMPORTANCE_LEVEL_HEADER.getName()); } } else if (rejectStorageWriteOnLowMemory) { log.debug("Received PUT request to {} without {} header. Going to handle this request with highest importance", req.uri(), IMPORTANCE_LEVEL_HEADER.getName()); } Long expire = -1L; // default infinit if (containsHeader(headers, EXPIRE_AFTER_HEADER)) { expire = getLong(headers, EXPIRE_AFTER_HEADER); if (expire == null) { req.resume(); rsp.setStatusCode(StatusCode.BAD_REQUEST.getStatusCode()); rsp.setStatusMessage("Invalid " + EXPIRE_AFTER_HEADER.getName() + " header: " + headers.get(EXPIRE_AFTER_HEADER.getName())); rsp.end(rsp.getStatusMessage()); log.error("{} header, invalid value: {}", EXPIRE_AFTER_HEADER.getName(), rsp.getStatusMessage()); return; } } if (log.isTraceEnabled()) { log.trace("RestStorageHandler put resource: {} with expire: {}", req.uri(), expire); } String lock = ""; Long lockExpire = 300L; // default 300s LockMode lockMode = LockMode.SILENT; // default if (containsHeader(headers, LOCK_HEADER)) { lock = headers.get(LOCK_HEADER.getName()); if (containsHeader(headers, LOCK_MODE_HEADER)) { try { lockMode = LockMode.valueOf(headers.get(LOCK_MODE_HEADER.getName()).toUpperCase()); } catch (IllegalArgumentException ex) { req.resume(); rsp.setStatusCode(StatusCode.BAD_REQUEST.getStatusCode()); rsp.setStatusMessage("Invalid " + LOCK_MODE_HEADER.getName() + " header: " + headers.get(LOCK_MODE_HEADER.getName())); rsp.end(rsp.getStatusMessage()); log.error("{} header, invalid value: {}", LOCK_MODE_HEADER.getName(), rsp.getStatusMessage(), ex); return; } } if (containsHeader(headers, LOCK_EXPIRE_AFTER_HEADER)) { lockExpire = getLong(headers, LOCK_EXPIRE_AFTER_HEADER); if (lockExpire == null) { req.resume(); rsp.setStatusCode(StatusCode.BAD_REQUEST.getStatusCode()); rsp.setStatusMessage("Invalid " + LOCK_EXPIRE_AFTER_HEADER.getName() + " header: " + headers.get(LOCK_EXPIRE_AFTER_HEADER.getName())); rsp.end(rsp.getStatusMessage()); log.error("{} header, invalid value: {}", LOCK_EXPIRE_AFTER_HEADER.getName(), rsp.getStatusMessage()); return; } } } boolean merge = (req.query() != null && req.query().contains("merge=true") && mimeTypeResolver.resolveMimeType(path).contains("application/json")); final String etag = headers.get(IF_NONE_MATCH_HEADER.getName()); boolean storeCompressed = Boolean.parseBoolean(headers.get(COMPRESS_HEADER.getName())); if (merge && storeCompressed) { req.resume(); rsp.setStatusCode(StatusCode.BAD_REQUEST.getStatusCode()); rsp.setStatusMessage("Invalid parameter/header combination: merge parameter and " + COMPRESS_HEADER.getName() + " header cannot be used concurrently"); rsp.end(rsp.getStatusMessage()); return; } storage.put(path, etag, merge, expire, lock, lockMode, lockExpire, storeCompressed, resource -> { final HttpServerResponse response = ctx.response(); ctx.request().resume(); if (resource.error) { final String message = (resource.errorMessage != null) ? resource.errorMessage : StatusCode.INTERNAL_SERVER_ERROR.getStatusMessage(); respondWith(response, StatusCode.INTERNAL_SERVER_ERROR, message); } else if (resource.rejected) { // TODO: Describe how 'rejected' maps to 'CONFLICT'. respondWith(response, StatusCode.CONFLICT, null); } else if (!resource.modified) { // TODO: Describe how 'not modified' relates to those headers. response.headers() .set(ETAG_HEADER.getName(), etag) .add(CONTENT_LENGTH.getName(), "0") ; respondWith(response, StatusCode.NOT_MODIFIED, null); } else if (resource instanceof CollectionResource) { // Its not allowed to override an existing collection by a resource. A // collection only can be GET or DELETE. response.headers() .add("Allow", "GET, DELETE") ; respondWith(response, StatusCode.METHOD_NOT_ALLOWED, null); } else if (resource instanceof DocumentResource) { if (!resource.exists) { // We'll arrive here when we try to put "/one/two/three" but "/one/two" already // exists as a resource. // See: "https://github.com/swisspush/vertx-rest-storage/blob/v2.5.7/src/main/java/org/swisspush/reststorage/RedisStorage.java#L837". // May there are also other cases this can happen. But I don't know about them. response.headers() .add("Allow", "GET, DELETE") ; respondWith(response, StatusCode.METHOD_NOT_ALLOWED, null); } else { // All checks successful. We'll now store contents of the resource. putResource_storeContentsOfDocumentResource(ctx, (DocumentResource) resource); } } else { // Cannot happen (Or at least theoretically it shouldn't). But in case someone // manages creating such a request somehow, we at least should properly // finalize our request. final HttpServerRequest request = ctx.request(); log.error("Unexpected case during 'PUT {}'", request.path()); respondWith(response, StatusCode.INTERNAL_SERVER_ERROR, "Unexpected case during PUT"); } }); } /** *

Helper method which completes response in case a {@link DocumentResource} * got PUT to storage.

* *

This method doesn't perform correctness checks for passed arguments. The * Caller is responsible to only call this method, if the resource is ready to * be stored.

*/ private void putResource_storeContentsOfDocumentResource(RoutingContext ctx, DocumentResource resource) { final HttpServerResponse response = ctx.response(); // Caller is responsible to do any 'error', 'exists', 'rejected' checks on the // resource. Therefore we simply go forward and store its content. final HttpServerRequest request = ctx.request(); resource.addErrorHandler((Throwable ex) -> { if (log.isDebugEnabled()) log.debug("stacktrace", exceptionFactory.newException("DocumentResource reports error", ex)); respondWith(response, StatusCode.INTERNAL_SERVER_ERROR, ex.getMessage()); }); // Complete response when resource written. resource.endHandler = nothing -> response.end(); // Close resource when payload fully read. request.endHandler(nothing -> resource.closeHandler.handle(null)); request.exceptionHandler(exc -> { // Report error // TODO: Evaluate which properties to set. Public interface documentation of // DocumentResource is de-facto non-existent. Therefore I've no idea // which properties to set how in this case here. resource.error = true; resource.errorMessage = exc.getMessage(); final Handler resourceErrorHandler = resource.errorHandler; if (resourceErrorHandler != null) { resourceErrorHandler.handle(exceptionFactory.newException(exc)); } }); final Pump pump = Pump.pump(request, resource.writeStream); pump.start(); } private void deleteResource(RoutingContext ctx) { final String path = cleanPath(ctx.request().path().substring(prefixFixed.length())); if (log.isTraceEnabled()) { log.trace("RestStorageHandler delete resource: {}", ctx.request().uri()); } String lock = ""; Long lockExpire = 300L; // default 300s LockMode lockMode = LockMode.SILENT; // default MultiMap headers = ctx.request().headers(); MultiMap params = ctx.request().params(); if (containsHeader(headers, LOCK_HEADER)) { lock = headers.get(LOCK_HEADER.getName()); if (containsHeader(headers, LOCK_MODE_HEADER)) { try { lockMode = LockMode.valueOf(headers.get(LOCK_MODE_HEADER.getName()).toUpperCase()); } catch (IllegalArgumentException ex) { ctx.request().resume(); ctx.response().setStatusCode(StatusCode.BAD_REQUEST.getStatusCode()); ctx.response().setStatusMessage("Invalid " + LOCK_MODE_HEADER.getName() + " header: " + headers.get(LOCK_MODE_HEADER.getName())); ctx.response().end(ctx.response().getStatusMessage()); log.error("{} header, invalid value: {}", LOCK_MODE_HEADER.getName(), ctx.response().getStatusMessage(), ex); return; } } if (containsHeader(headers, LOCK_EXPIRE_AFTER_HEADER)) { lockExpire = getLong(headers, LOCK_EXPIRE_AFTER_HEADER); if (lockExpire == null) { ctx.request().resume(); ctx.response().setStatusCode(StatusCode.BAD_REQUEST.getStatusCode()); ctx.response().setStatusMessage("Invalid " + LOCK_EXPIRE_AFTER_HEADER.getName() + " header: " + headers.get(LOCK_EXPIRE_AFTER_HEADER.getName())); ctx.response().end(ctx.response().getStatusMessage()); log.error("{} header, invalid value: {}", LOCK_EXPIRE_AFTER_HEADER.getName(), ctx.response().getStatusMessage()); return; } } } storage.delete(path, lock, lockMode, lockExpire, confirmCollectionDelete, getBoolean(params, RECURSIVE_PARAMETER), resource -> { var rsp = ctx.response(); if (resource.rejected) { rsp.setStatusCode(StatusCode.CONFLICT.getStatusCode()); rsp.setStatusMessage(StatusCode.CONFLICT.getStatusMessage()); rsp.end(); } else if (resource.error) { rsp.setStatusCode(StatusCode.BAD_REQUEST.getStatusCode()); rsp.setStatusMessage(StatusCode.BAD_REQUEST.getStatusMessage()); String message = StatusCode.BAD_REQUEST.getStatusMessage(); if (resource.errorMessage != null) { message = message + ": " + resource.errorMessage; } rsp.end(message); } else if (!resource.exists) { if (return200onDeleteNonExisting) { rsp.end(); // just say "200 OK" - ignore that the resource-to-be-deleted was not present } else { ctx.request().response().setStatusCode(StatusCode.NOT_FOUND.getStatusCode()); ctx.request().response().setStatusMessage(StatusCode.NOT_FOUND.getStatusMessage()); ctx.request().response().end(StatusCode.NOT_FOUND.toString()); } } else { ctx.request().response().end(); } }); } private boolean checkMaxSubResourcesCount(HttpServerRequest request, int subResourcesArraySize) { MultiMap headers = request.headers(); // get max expand value from request header or use the configuration value as fallback Integer maxStorageExpand = getInteger(headers, MAX_EXPAND_RESOURCES_HEADER, maxStorageExpandSubresources); if (maxStorageExpand < subResourcesArraySize) { respondWith(request.response(), StatusCode.PAYLOAD_TOO_LARGE, "Resources provided: "+subResourcesArraySize+". Allowed are: " + maxStorageExpand); return true; } return false; } private void storageExpand(RoutingContext ctx) { HttpServerRequest request = ctx.request(); if (!containsParam(request.params(), STORAGE_EXPAND_PARAMETER)) { respondWithNotAllowed(request); } else { request.bodyHandler(bodyBuf -> { List subResourceNames = new ArrayList<>(); try { JsonObject body = new JsonObject(bodyBuf); JsonArray subResourcesArray = body.getJsonArray("subResources"); if (subResourcesArray == null) { respondWithBadRequest(request, "Bad Request: Expected array field 'subResources' with names of resources"); return; } if (checkMaxSubResourcesCount(request, subResourcesArray.size())) { return; } for (int i = 0; i < subResourcesArray.size(); i++) { subResourceNames.add(subResourcesArray.getString(i)); } ResourceNameUtil.replaceColonsAndSemiColonsInList(subResourceNames); } catch (RuntimeException ex) { log.warn("KISS handler is not interested in error details. I'll report them here then.", ex); respondWithBadRequest(request, "Bad Request: Unable to parse body of storageExpand POST request"); return; } final String path = cleanPath(request.path().substring(prefixFixed.length())); final String etag = request.headers().get(IF_NONE_MATCH_HEADER.getName()); storage.storageExpand(path, etag, subResourceNames, resource -> { var rsp = ctx.response(); if (resource.error) { rsp.setStatusCode(StatusCode.CONFLICT.getStatusCode()); rsp.setStatusMessage(StatusCode.CONFLICT.getStatusMessage()); String message = StatusCode.CONFLICT.getStatusMessage(); if (resource.errorMessage != null) { message = resource.errorMessage; } rsp.end(message); return; } if (resource.invalid) { rsp.setStatusCode(StatusCode.INTERNAL_SERVER_ERROR.getStatusCode()); rsp.setStatusMessage(StatusCode.INTERNAL_SERVER_ERROR.getStatusMessage()); String message = StatusCode.INTERNAL_SERVER_ERROR.getStatusMessage(); if (resource.invalidMessage != null) { message = resource.invalidMessage; } rsp.end(new JsonObject().put("error", message).encode()); return; } if (!resource.modified) { rsp.setStatusCode(StatusCode.NOT_MODIFIED.getStatusCode()); rsp.setStatusMessage(StatusCode.NOT_MODIFIED.getStatusMessage()); rsp.headers().set(ETAG_HEADER.getName(), etag); rsp.headers().add(CONTENT_LENGTH.getName(), "0"); rsp.end(); return; } if (resource.exists) { if (log.isTraceEnabled()) { log.trace("RestStorageHandler resource is a DocumentResource: {}", request.uri()); } String mimeType = mimeTypeResolver.resolveMimeType(path); final DocumentResource documentResource = (DocumentResource) resource; if (documentResource.etag != null && !documentResource.etag.isEmpty()) { rsp.headers().add(ETAG_HEADER.getName(), documentResource.etag); } rsp.headers().add(CONTENT_LENGTH.getName(), "" + documentResource.length); rsp.headers().add(CONTENT_TYPE.getName(), mimeType); final Pump pump = Pump.pump(documentResource.readStream, rsp); documentResource.readStream.endHandler(nothing -> { documentResource.closeHandler.handle(null); rsp.end(); }); pump.start(); // TODO: exception handlers } else { if (log.isTraceEnabled()) { log.trace("RestStorageHandler Could not find resource: {}", request.uri()); } rsp.setStatusCode(StatusCode.NOT_FOUND.getStatusCode()); rsp.setStatusMessage(StatusCode.NOT_FOUND.getStatusMessage()); rsp.end(StatusCode.NOT_FOUND.toString()); } }); }); } } //////////////////////////// // End Router handling // //////////////////////////// private Result checkHttpAuthenticationConfiguration(ModuleConfiguration modConfig) { if(modConfig.isHttpRequestHandlerAuthenticationEnabled()) { if(StringUtil.isNullOrEmpty(modConfig.getHttpRequestHandlerUsername()) || StringUtil.isNullOrEmpty(modConfig.getHttpRequestHandlerPassword())) { String msg = "HTTP API authentication is enabled but credentials are missing"; log.warn(msg); return Result.err(msg); } return Result.ok(true); } return Result.ok(false); } private String cleanPath(String value) { value = value.replaceAll("\\.\\.", "").replaceAll("\\/\\/", "/"); while (value.endsWith("/")) { value = value.substring(0, value.length() - 1); } if (value.isEmpty()) { return "/"; } return value; } public static class OffsetLimit { public OffsetLimit(int offset, int limit) { this.offset = offset; this.limit = limit; } public int offset; public int limit; } private void respondWithNotAllowed(HttpServerRequest request) { respondWith(request.response(), StatusCode.METHOD_NOT_ALLOWED, null); } private void respondWithBadRequest(HttpServerRequest request, String responseMessage) { respondWith(request.response(), StatusCode.BAD_REQUEST, responseMessage); } private void respondWith(HttpServerResponse response, StatusCode statusCode, String responseBody) { response.setStatusCode(statusCode.getStatusCode()); response.setStatusMessage(statusCode.getStatusMessage()); if (responseBody != null) { response.putHeader(CONTENT_TYPE.getName(), "text/plain"); response.end(responseBody); } else { response.end(); } } private String collectionName(String path) { if (path.equals("/") || path.isEmpty()) { return "root"; } else { return path.substring(path.lastIndexOf("/") + 1); } } private String htmlPath(String path) { if (path.equals("/")) { return "/"; } StringBuilder sb = new StringBuilder(); StringBuilder p = new StringBuilder(); String[] parts = path.split("/"); for (int i = 0; i < parts.length; i++) { String part = parts[i]; p.append(part); p.append("/"); if (i < parts.length - 1) { sb.append(" "); sb.append(part); sb.append(" > "); } else { sb.append(" "); sb.append(part); } } return sb.toString(); } }




© 2015 - 2025 Weber Informatics LLC | Privacy Policy