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

org.swisspush.gateleen.routing.Forwarder Maven / Gradle / Ivy

Go to download

Middleware library based on Vert.x to build advanced JSON/REST communication servers

The newest version!
package org.swisspush.gateleen.routing;

import io.vertx.core.Handler;
import io.vertx.core.Vertx;
import io.vertx.core.buffer.Buffer;
import io.vertx.core.http.HttpClient;
import io.vertx.core.http.HttpClientRequest;
import io.vertx.core.http.HttpServerRequest;
import io.vertx.core.json.JsonObject;
import io.vertx.ext.web.RoutingContext;
import org.slf4j.Logger;
import org.swisspush.gateleen.core.http.RequestLoggerFactory;
import org.swisspush.gateleen.monitoring.MonitoringHandler;
import org.swisspush.gateleen.core.storage.ResourceStorage;
import org.swisspush.gateleen.core.util.ResponseStatusCodeLogUtil;
import org.swisspush.gateleen.core.util.StatusCode;
import org.swisspush.gateleen.core.util.StringUtils;
import org.swisspush.gateleen.logging.LoggingHandler;
import org.swisspush.gateleen.logging.LoggingResourceManager;

import java.util.Base64;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.TimeoutException;
import java.util.regex.Pattern;

/**
 * Forwards requests to the backend.
 *
 * @author https://github.com/lbovet [Laurent Bovet]
 */
public class Forwarder implements Handler {

    private String userProfilePath;
    private HttpClient client;
    private Pattern urlPattern;
    private String target;
    private Rule rule;
    private String base64UsernamePassword;
    private LoggingResourceManager loggingResourceManager;
    private MonitoringHandler monitoringHandler;
    private ResourceStorage storage;
    private Vertx vertx;

    private static final String ON_BEHALF_OF_HEADER = "x-on-behalf-of";
    private static final String USER_HEADER = "x-rp-usr";
    private static final String USER_HEADER_PREFIX = "x-user-";
    private static final String ETAG_HEADER = "Etag";
    private static final String IF_NONE_MATCH_HEADER = "if-none-match";
    private static final String SELF_REQUEST_HEADER = "x-self-request";

    public Forwarder(Vertx vertx, HttpClient client, Rule rule, final ResourceStorage storage, LoggingResourceManager loggingResourceManager, MonitoringHandler monitoringHandler, String userProfilePath) {
        this.vertx = vertx;
        this.client = client;
        this.rule = rule;
        this.loggingResourceManager = loggingResourceManager;
        this.monitoringHandler = monitoringHandler;
        this.storage = storage;
        this.urlPattern = Pattern.compile(rule.getUrlPattern());
        this.target = rule.getHost() + ":" + rule.getPort();
        this.userProfilePath = userProfilePath;
        if (rule.getUsername() != null && !rule.getUsername().isEmpty()) {
            String password = rule.getPassword() == null ? null : rule.getPassword().trim();
            base64UsernamePassword = Base64.getEncoder().encodeToString((rule.getUsername().trim() + ":" + password).getBytes());
        }
    }

    private Map createProfileHeaderValues(JsonObject profile, Logger log) {
        Map profileValues = new HashMap<>();
        if (rule.getProfile() != null) {
            String[] ruleProfile = rule.getProfile();
            for (int i = 0; i < ruleProfile.length; i++) {
                String headerKey = ruleProfile[i];
                String headerValue = profile.getString(headerKey);
                if (headerKey != null && headerValue != null) {
                    profileValues.put(USER_HEADER_PREFIX + headerKey, headerValue);
                    log.debug("Sending header-information for key " + headerKey + ", value = " + headerValue);
                } else {
                    if (headerKey != null) {
                        log.debug("We should send profile information '" + headerKey + "' but this information was not found in profile.");
                    } else {
                        log.debug("We should send profile information but header key is null.");
                    }
                }
            }
        } else {
            log.debug("rule.profile is null, this rule will not send profile information.");
        }
        return profileValues;
    }

    @Override
    public void handle(final RoutingContext ctx) {
        handle(ctx.request(), null);
    }

    /**
     * Allows to handle a request which was already consumed.
     * If the parameter bodyData is not null, the
     * request was consumed and you can't read the body of the
     * request again.
     *
     * @param req - the original request
     * @param bodyData - a buffer with the body data, null if the request
     * was not yet consumed
     */
    public void handle(final HttpServerRequest req, final Buffer bodyData) {
        monitoringHandler.updateRequestsMeter(target, req.uri());
        monitoringHandler.updateRequestPerRuleMonitoring(req, rule.getName());
        final String targetUri = urlPattern.matcher(req.uri()).replaceAll(rule.getPath()).replaceAll("\\/\\/", "/");
        final Logger log = RequestLoggerFactory.getLogger(Forwarder.class, req);
        log.debug("Forwarding request: " + req.uri() + " to " + rule.getScheme() + "://" + target + targetUri + " with rule " + rule.getRuleIdentifier());
        final String userId = extractUserId(req, log);

        if (userId != null && rule.getProfile() != null && userProfilePath != null) {
            log.debug("Get profile information for user '" + userId + "' to append to headers");
            String userProfileKey = String.format(userProfilePath, userId);
            req.pause(); // pause the request to avoid problems with starting another async request (storage)
            storage.get(userProfileKey, buffer -> {
                Map profileHeaderMap = new HashMap<>();
                if (buffer != null) {
                    JsonObject profile = new JsonObject(buffer.toString());
                    profileHeaderMap = createProfileHeaderValues(profile, log);
                    log.debug("Got profile information of user '" + userId + "'");
                    log.debug("Going to send parts of the profile in header: " + profileHeaderMap);
                } else {
                    log.debug("No profile information found in local storage for user '" + userId + "'");
                }
                handleRequest(req, bodyData, targetUri, log, profileHeaderMap);
            });
        } else {
            handleRequest(req, bodyData, targetUri, log, null);
        }
    }

    /**
     * Returns the userId defined in the on-behalf-of-header if provided, the userId from user-header otherwise.
     * 
     * @param request request
     * @param log log
     */
    private String extractUserId(HttpServerRequest request, Logger log) {
        String onBehalfOf = StringUtils.getStringOrEmpty(request.headers().get(ON_BEHALF_OF_HEADER));
        if (StringUtils.isNotEmpty(onBehalfOf)) {
            log.debug("Using values from x-on-behalf-of header instead of taking them from x-rp-usr header");
            return onBehalfOf;
        } else {
            return request.headers().get(USER_HEADER);
        }
    }

    private void handleRequest(final HttpServerRequest req, final Buffer bodyData, final String targetUri, final Logger log, final Map profileHeaderMap) {
        final LoggingHandler loggingHandler = new LoggingHandler(loggingResourceManager, req);

        final String uniqueId = req.headers().get("x-rp-unique_id");
        final long startTime = monitoringHandler.startRequestMetricTracking(rule.getMetricName(), req.uri());

        final HttpClientRequest cReq = prepareRequest(req, targetUri, log, profileHeaderMap, loggingHandler, startTime);

        cReq.setTimeout(rule.getTimeout());
        // Fix unique ID header name for backends not able to handle underscore in header names.
        cReq.headers().setAll(req.headers());

        if (!ResponseStatusCodeLogUtil.isRequestToExternalTarget(target)) {
            cReq.headers().set(SELF_REQUEST_HEADER, "");
        }

        if (uniqueId != null) {
            cReq.headers().set("x-rp-unique-id", uniqueId);
        }
        setProfileHeaders(log, profileHeaderMap, cReq);
        // https://jira/browse/NEMO-1494
        // the Host has to be set, if only added it will add a second value and not overwrite existing ones
        cReq.headers().set("Host", target.split("/")[0]);
        if (base64UsernamePassword != null) {
            cReq.headers().set("Authorization", "Basic " + base64UsernamePassword);
        }

        setStaticHeaders(cReq);

        cReq.setChunked(true);

        if (rule.getTimeout() > 0) {
            cReq.setTimeout(rule.getTimeout());
        }
        installExceptionHandler(req, targetUri, startTime, cReq);

        /*
         * If no bodyData is available
         * this means, that the request body isn't
         * consumed yet. So we can use the regular
         * request for the data.
         * If the body is already consumed, we use
         * the buffer bodyData.
         */
        if (bodyData == null) {
            req.handler(data -> {
                cReq.write(data);
                loggingHandler.appendRequestPayload(data);
            });
            req.endHandler(v -> cReq.end());
        } else {
            loggingHandler.appendRequestPayload(bodyData);
            cReq.end(bodyData);
        }

        loggingHandler.request(cReq.headers());

        req.resume();
    }

    private void setStaticHeaders(HttpClientRequest cReq) {
        if (rule.getStaticHeaders() != null) {
            for (Map.Entry entry : rule.getStaticHeaders().entrySet()) {
                cReq.headers().set(entry.getKey(), entry.getValue());
            }
        }
    }

    private void setProfileHeaders(Logger log, Map profileHeaderMap, HttpClientRequest cReq) {
        if (profileHeaderMap != null && !profileHeaderMap.isEmpty()) {
            log.debug("Putting partial profile to header for the backend request (profileHeaderMap).");
            for (Map.Entry entry : profileHeaderMap.entrySet()) {
                cReq.headers().set(entry.getKey(), entry.getValue());
            }
        }
    }

    private void installExceptionHandler(final HttpServerRequest req, final String targetUri, final long startTime, HttpClientRequest cReq) {
        cReq.exceptionHandler(exception -> {
            monitoringHandler.stopRequestMetricTracking(rule.getMetricName(), startTime, req.uri());
            if (exception instanceof TimeoutException) {
                error("Timeout", req, targetUri);
                req.response().setStatusCode(StatusCode.TIMEOUT.getStatusCode());
                req.response().setStatusMessage(StatusCode.TIMEOUT.getStatusMessage());
                try {
                    ResponseStatusCodeLogUtil.debug(req, StatusCode.TIMEOUT, Forwarder.class);
                    req.response().end(req.response().getStatusMessage());
                } catch (IllegalStateException e) {
                    // ignore because maybe already closed
                }
            } else {
                error(exception.getMessage(), req, targetUri);
                req.response().setStatusCode(StatusCode.SERVICE_UNAVAILABLE.getStatusCode());
                req.response().setStatusMessage(StatusCode.SERVICE_UNAVAILABLE.getStatusMessage());
                try {
                    ResponseStatusCodeLogUtil.debug(req, StatusCode.SERVICE_UNAVAILABLE, Forwarder.class);
                    req.response().end(req.response().getStatusMessage());
                } catch (IllegalStateException e) {
                    // ignore because maybe already closed
                }
            }
        });
    }

    private HttpClientRequest prepareRequest(final HttpServerRequest req, final String targetUri, final Logger log, final Map profileHeaderMap, final LoggingHandler loggingHandler, final long startTime) {
        return client.request(req.method(), targetUri, cRes -> {
            monitoringHandler.stopRequestMetricTracking(rule.getMetricName(), startTime, req.uri());
            loggingHandler.setResponse(cRes);
            req.response().setStatusCode(cRes.statusCode());
            req.response().setStatusMessage(cRes.statusMessage());

            int statusCode = cRes.statusCode();

            // translate with header info
            int translatedStatus = Translator.translateStatusCode(statusCode, req.headers());

            // nothing changed?
            if (statusCode == translatedStatus) {
                translatedStatus = Translator.translateStatusCode(statusCode, rule, log);
            }

            // set the statusCode (if nothing hapend, it will remain the same)
            statusCode = translatedStatus;

            req.response().setStatusCode(statusCode);

            req.response().headers().addAll(cRes.headers());
            if (profileHeaderMap != null && !profileHeaderMap.isEmpty()) {
                req.response().headers().addAll(profileHeaderMap);
            }
            if (req.response().getStatusCode() == StatusCode.NOT_MODIFIED.getStatusCode()) {
                req.response().headers().add("Content-Length", "0");
            }
            if (!req.response().headers().contains("Content-Length")) {
                req.response().setChunked(true);
            }

            final String responseEtag = cRes.headers().get(ETAG_HEADER);
            if (responseEtag != null && !responseEtag.isEmpty()) {
                cRes.bodyHandler(data -> {
                    String ifNoneMatchHeader = req.headers().get(IF_NONE_MATCH_HEADER);
                    if (responseEtag.equals(ifNoneMatchHeader)) {
                        req.response().setStatusCode(StatusCode.NOT_MODIFIED.getStatusCode());
                        req.response().setStatusMessage(StatusCode.NOT_MODIFIED.getStatusMessage());
                        ResponseStatusCodeLogUtil.debug(req, StatusCode.NOT_MODIFIED, Forwarder.class);
                        req.response().end();
                    } else {
                        ResponseStatusCodeLogUtil.debug(req, StatusCode.fromCode(req.response().getStatusCode()), Forwarder.class);
                        req.response().end(data);
                    }
                    loggingHandler.appendResponsePayload(data);
                    vertx.runOnContext(event -> loggingHandler.log());
                });
            } else {
                cRes.handler(data -> {
                    req.response().write(data);
                    loggingHandler.appendResponsePayload(data);
                });
                cRes.endHandler(v -> {
                    try {
                        req.response().end();
                        ResponseStatusCodeLogUtil.debug(req, StatusCode.fromCode(req.response().getStatusCode()), Forwarder.class);
                    } catch (IllegalStateException e) {
                        // ignore because maybe already closed
                    }
                    vertx.runOnContext(event -> loggingHandler.log());
                });
            }

            cRes.exceptionHandler(exception -> {
                error("Problem with backend: " + exception.getMessage(), req, targetUri);
                req.response().setStatusCode(StatusCode.INTERNAL_SERVER_ERROR.getStatusCode());
                req.response().setStatusMessage(StatusCode.INTERNAL_SERVER_ERROR.getStatusMessage());
                try {
                    ResponseStatusCodeLogUtil.debug(req, StatusCode.INTERNAL_SERVER_ERROR, Forwarder.class);
                    req.response().end(req.response().getStatusMessage());
                } catch (IllegalStateException e) {
                    // ignore because maybe already closed
                }
            });
        });
    }

    private void error(String message, HttpServerRequest request, String uri) {
        RequestLoggerFactory.getLogger(Forwarder.class, request).error(rule.getScheme() + "://" + target + uri + " " + message);
    }
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy